上篇讲了关于手写 Promise , 从类的设计, 构造函数设计, then 方法实现, catch 方法借助 then 方法实现, then 方法的核心优化, 包括多次调用, 链式调用等, 最后再补充了一些常用的类方法, 如 all, allSettled, any 等实现, 整体过程还是比较有难度的, 体现在状态的变更, 代码执行顺序的控制等, 也算是 js 一个重要知识点吧.
本篇则开始学习 js 中的迭代器和生成器, 只有先搞明白这俩兄弟, 才会对后续将 async ... await + Promise 有深刻认知.
认识迭代器
迭代器 (iterator) 是使用户能在容器对象上, 安全, 统一地访问容器元素的方法机制协议, 而无需关系容器内部实现细节
或者说迭代器是一种对象, 它提供了一种按顺序访问集合 (数组, 列表, 链表, 集合, 映射等) 中的每个元素的方法, 而无需让开发者了解其底层实现 (抽象接口). 它通常实现一个 next() 方法, 犯病一个包含两个属性的对象:
{value: 当前值, done: 是否遍历完成 }
它本质上是一种设计模式, 在语言中通过接口和协议规范, 在运行时表现为一个具有状态和行为的对象. 可以将它类比为一个地铁的闸机系统:
- 设计模式: "一人一票, 顺序通过"
- 编程规范: 所有地铁站必须刷卡
- 接口定义: 必须实现 validate(cart) 和 open() 方法
- 也是对象: 每个地铁站都有一台闸机
- 数据结构: 不存数据 (乘客), 只控制访问顺序
迭代器的好处或者说应用场景在于:
- **统一接口: **不论遍历字符串, 数组, Map, 自定义结构等, 都可以用
for...of或.next()来遍历 - **延迟计算: **迭代器可以"按需" 生成值, 而非一次性生成所有数据 (生成器)
- 内存提效: 适用于处理大数据流或者无限序列 (斐波那契数列等)
- 解耦结构: 数据结构和遍历元素拆分, 让遍历元素部分由迭代器实现
js 中的迭代器
**在 js 中, 迭代器也是一个具体的对象, 这个对象需要符合迭代器协议: **
- 迭代器协议, 定义了产生一系列值的标准方式
- 在 js 中标准就是一个特定的
next()方法
这个 next() 方法需要满足如下的要求:
- 是一个
无参或者仅一个参数的函数, 返回一个应当拥有如下两个属性的对象 - done (boolean)
- value
// 迭代器 认识, 也是一个对象const names = ['youge', 'cj', 'yaya']// 创建一个迭代器对象, 来访问 names 数组
let index = 0
const namesIterator = {next: function() {if (index < names.length) {return { done: false, value: names[index++] }} else {return { done: true, value: undefined }}}
}// 通过 迭代器的 next() 方法不断去访问数组元素
console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())// { done: false, value: 'youge' }
// { done: false, value: 'cj' }
// { done: false, value: 'yaya' }
// { done: true, value: undefined }
// { done: true, value: undefined }
继续来封装一个生成迭代器的函数.
// 生成迭代器的函数, 返回一个对象, 包含 next()// 数组迭代器
function createArrayIterator(arr) {let index = 0return {next: function() {if (index < arr.length) {return { done: false, value: arr[index++]}} else {return { done: true, value: undefined }}}}
}const names = ['youge', 'cj', 'yaya']
const nums = [10, 20, 30]const namesIterator = createArrayIterator(names)console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())// { done: false, value: 'youge' }
// { done: false, value: 'cj' }
// { done: false, value: 'yaya' }
// { done: true, value: undefined }
// { done: true, value: undefined }// nums数组 的迭代器
const numsIterator = createArrayIterator(nums)console.log(numsIterator.next())
console.log(numsIterator.next())
console.log(numsIterator.next()) // { done: false, value: 30 }
console.log(numsIterator.next()) // { done: true, value: undefined }// 创建一个无限的迭代器
function createNumIterator() {let index = 0return {next: function() {return { done: false, value: index++ }}}
}const numIterator = createNumIterator()console.log(numIterator.next()) // 0
console.log(numIterator.next()) // 1
console.log(numIterator.next()) // 2
console.log(numIterator.next()) // 3
console.log(numIterator.next()) // 4
可迭代对象
上面认识迭代器的时候, 这个代码分为了 3个部分:
- 创建将要被迭代的数组 (全局)
- 创建一个对象, 里面实现了一个叫
next()的方法来获取数组数据 (全局) - 创建一个全局变量 index 来控制 对象 里面的 next 方法, 来访问数组对象
现在要来思考的是, 如何将上面这有紧密联系但是有互相分开的3部分组合到一起, 即统一放在同一个对象中.
// 组合到一起const newObj {names;index;obj.next();
}
这个能组合到一起的对象, 就称为 可迭代对象
- 该对象实现了
iterable protocol可迭代协议 - 该协议必须实现
@@iterator方法, 对应在代码中实现[Symbol.iterator]方法即可
const iterableObje= { [Symbol.iterator]: function() { return 迭代器 } }
迭代器
是一个对象, 它实现了 迭代器协议, 表现为该对象有一个 .next() 方法, 并返回 { value, done } 的结构
可迭代对象
也是一个对象, 它实现了 可迭代协议, 表现为该对象有一个 Symbol.ierator 方法, 该方法返回一个 实现了迭代器协议的对象
// 可迭代对象 与 迭代器const arr = [1, 2, 3]
console.log(arr[Symbol.iterator]) // 是个函数, arr 是可迭代对象// 数组也是对象, 内置实现了可迭代协议, 可以调用 Symbol.iterator
// 该方法返回一个迭代器, 可以用它来进行 .next() 获取元素
const iter = arr[Symbol.iterator]()console.log(iter.next())
console.log(iter.next())
console.log(iter.next())
console.log(iter.next())// { value: 1, done: false }
// { value: 2, done: false }
// { value: 3, done: false }
// { value: undefined, done: true }
| 对比项 | 可迭代对象(Iterable) | 迭代器(Iterator) |
|---|---|---|
| 本质 | 对象 | 对象 |
| 核心特征 | 有 [Symbol.iterator]() 方法 |
有 .next() 方法 |
| 协议 | 实现 可迭代协议 | 实现 迭代器协议 |
| 作用 | 能被 for...of、...、yield* 等消费 |
负责实际遍历,返回 {value, done} |
| 是否可遍历 | ✅ 可以被遍历 | ✅ 可以被遍历(因为迭代器自己也是可迭代的) |
| 典型例子 | Array, String, Map, Set, arguments |
array[Symbol.iterator]() 的返回值 |
厘清概念之后, 现对之前的数组, 迭代器, 迭代获取数组元素这个过程进行封装, 即将 nams 数组变为可迭代对象.
// 可迭代对象const iterableObj = {names: ['youge', 'cj', 'yaya'],// Symbol.iterator 用 [] 包起来是让其作为字面量对象的 "计算属性"[Symbol.iterator]: function() {let index = 0return {next: () => { // 箭头函数无绑定this, 因此 next() 中的 this 会找到父级的 namesif (index < this.names.length) {return { done: false, value: this.names[index++]}} else {return { done: true, value: undefined}}}}}
}// 此时 iterableObj 对象是一个 可迭代对象
console.log(iterableObj[Symbol.iterator]) // 函数// 第一次调用
const iterator = iterableObj[Symbol.iterator]()
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())
console.log(iterator.next())// 第二次调用
const iterator2 = iterableObj[Symbol.iterator]()
console.log(iterator2.next())// for ... of 可以遍历, 可迭代对象
for (const item of iterableObj) {console.log(item) // youge, cj, yaya
}
注意点:
- Symbol.iterator 用 "[ ]" 包起来是让其作为字面量对象的 "计算属性"
- 利用箭头函数无绑定 this, 因此 next() 中的 this 会找到父级的 names
- for ... of 可以遍历, 可迭代对象
可见在 js 中的这个 this 的理解是非常重要的, 这里就用到了隐式调用原则嘛.
然后也解决了困扰我很久的问题: js 普通对象为啥不能用 for..of 遍历? 因为普通对象没有实现可迭代协议呀.
// 普通对象不可用 for...of 迭代 const obj = {name: 'youge',age: 18
}for (const item of obj) {console.log(item)
}// TypeError: obj is not iterable
可以理解这个 for...of 也是一个语法糖, 它做的事情就是用迭代器不断调用 .next() 获取元素, 直到 done:true 的时候, 则停止.
内置迭代器对象
像我们常用的 String, Array, Map, Set, arguments, NodeList 等原生对象, 都已实现了可迭代协议, 通过它们所产生的对象, 都是一个可迭代对象.
// 内置了`代器的对象// 字符串
const str = "ab c"
for (const s of str) {console.log(s) // a, b, " "
}// 数组
const arr = [1, 2]const arrIter = arr[Symbol.iterator]()
console.log(arrIter.next()) // { value: 1, done: false }
console.log(arrIter.next()) // { value: 2, done: false }
console.log(arrIter.next()) // { value: undefined, done: true}// set / map
const set = new Set()
set.add(10)
set.add(20)console.log(set[Symbol.iterator]) // [Function: values]for (const item of set) {console.log(item) // 10, 20
}// map
const map = new Map()
map.set('name', 'youge')
map.set(() => {}, 'cjj')for (const item of map) {console.log(item)// [ 'name', 'youge' ]// [ [Function (anonymous)], 'cjj' ]
}// 函数中的 arguments
function foo() {for (const arg of arguments) {console.log(arg) // 1, 2, 3}
}foo(1, 2, 3)
这些内置对象, 都是可迭代对象, 原因就是它们都实现了 Symbol.iterable 方法, 它会返回迭代器, 内部实现元素的遍历啦.
可迭代对象的应用场景
- js 语法提升: for ...of; 展开表达式; yield; 解构赋值等
- 创建对象时: new Mapt([iterable]); new Set([iterable]), new WeakMap([iterable]) 等
- 方法调用时: Promise.all(iterable); Promise.all(iterable); Array.from(iterable) 等
// 可迭代对象应用// 1. for...of 语法支持
const arr = [1, 2, 3]
for (const item of arr) {console.log(item) // 1, 2, 3
}// 2. 展开运算符
const iterableObj = {names: ['youge', 'cj', 'yaya'],[Symbol.iterator]: function() {let index = 0return {next: () => {if (index < this.names.length) {return { done: false, value: this.names[index++] }} else {return { done: true, value: undefined }}}}}
}const names = ['bob', 'jack']
const newNames = [...names, ...iterableObj]// [ 'bob', 'jack', 'youge', 'cj', 'yaya' ]
console.log(newNames)// ES9 新增普通对象特性, 虽不可迭代, 但要求支持展开运算符
// 猜测实现原理 for (const entry obj.entries ) {} 组装 key, value 就搞定了
const obj = { name: 'youge', age: 18 }
console.log({...obj}) // { name: 'youge', age: 18 }// 3. 解构语法
const [name1, name2] = names
console.log(name1, name2) // bob, jackconst { name, age } = obj
console.log(name, age) // youge, 18// 4. 创建其他对象时
const set1 = new Set(iterableObj)
const set2 = new Set(arr)// 可迭代对象, 转为数组, 如 arguments
const arr1 = Array.from(iterableObj)// 5. Promise.all(), 传普通值会自动 Promise.resolve(普通值)
Promise.all(iterableObj).then(res => {console.log("res: ", res) // res: [ 'youge', 'cj', 'yaya' ]
})
自定义类的可迭代实现
上述通过 Array, Set, Map, String 等类, 创建出来的对象都是可迭代对象.
现在要来实现一个自己的类, 使其创建出来的对象也是可迭代的, 即在类中实现 [Symbol.iterator] 方法即可
用一个案例: 创建一个 Classroom 的类:
- 每个教室有自己的编号, 楼层, 当前教室的学生
- 教室可以新进来学生 (push)
- 创建的教室对象是可迭代对象
// 自定义类 - 实现可迭代功能class Classroom {constructor(id, floor, students) {this.id = id this.floor = floor this.students = students }entry(newStudent) {this.students.push(newStudent)}
}const classroom = new Classroom("301", "3楼", ['bob', 'jack'])classroom.entry('cj')// 需求: 将教室对象都迭代出来
for (const item of classroom) {console.log(item)
}
当前是不能使用 for...of 语法的, 因为我们这个自定义的类, 没有实现可迭代协议. 则我们在对象方法上去实现这个 Symbol.iterator 的方法即可.
// 自定义类 - 实现可迭代功能class Classroom {constructor(id, floor, students) {this.id = id this.floor = floor this.students = students }entry(newStudent) {this.students.push(newStudent)}// 让对象可迭代, 添加 [Symbol.iterator] 方法[Symbol.iterator]() {let index = 0return {next: () => {if (index < this.students.length) {return { done: false, value: this.students[index++] }} else {return { done: true, value: undefined }}}}}
}const classroom = new Classroom("301", "3楼", ['bob', 'jack'])classroom.entry('cj')// 需求: 将教室内学生都迭代出来
for (const item of classroom) {console.log(item) // bob, jack, cj
}
生成器
生成器 (Generator) 是一种特殊的函数, 它可以在函数执行中暂停, 之后再从暂停的地方恢复执行.
它返回一个生成器对象, 该对象既是迭代器, 也是可迭代对象
它提供了一种暂停和恢复函数执行的能力, 是实现惰性求值, 异步编程, 迭代器 等高级功能的核心工具.
// 普通函数的执行, 不能暂停function foo() {console.log(100)console.log(200)console.log(300)
}foo() // 需求: 在 打印 200 时先暂停, 晚点再往后执行
普通函数不能控制 "中途暂停", 此功能在当前知识体系下, 是不能实现的.
生成器函数
生成器函数也是一个函数, 但和普通函数有区别:
- 需要在
function的后面加一个符号* - 可以通过
yield关键字来控制函数执行流程 - 返回值是一个
生成器对象, 它是一种特殊的迭代器
// 生成器函数function* foo() {console.log('函数开始执行...')console.log('第一段代码: ', 100)yieldconsole.log("第二代代码: ", 200)yieldconsole.log("第三段代码: ", 300)yield console.log('函数执行结束')
}// 调用 生成器函数时, 会返回一个生成器对象, 特殊迭代器
const generator = foo()
// 执行第一段代码, 第一个 yield
generator.next()
// 执行第二段代码
generator.next()
// 执行第三段代码
generator.next()
generator.next() // 函数执行结束
即通过 yield 来控制函数的执行顺序, 然后调用生成器函数, 返回的是一个生成器对象, 它是一个特殊的迭代器, 因此可以用它来调用 .next() 方法, 对应于 yield 的分段代码.
// 生成器函数-执行流程 // 当遇到 yield 时, 暂停函数执行
// 当遇到 return 时, 生成器停止执行 function* foo() {console.log('函数开始执行...')console.log('第一段代码: ', 100)yield 111console.log("第二代代码: ", 200)yield console.log("第三段代码: ", 300)yield 333console.log('函数执行结束')return 'over'
}// 调用 生成器函数时, 会返回一个生成器对象, 特殊迭代器
const generator = foo() // 生成器对象 的 next() 也有返回值
console.log(generator.next())
console.log(generator.next() )
console.log(generator.next() )console.log(generator.next() )// 函数执行结束// 函数开始执行...
// 第一段代码: 100
// { value: 111, done: false }// 第二代代码: 200
// { value: undefined, done: false }// 第三段代码: 300
// { value: 333, done: false }// 函数执行结束
// { value: 'over', done: true }
通过 yield 可以有返回值, 对应到 { value, done} 中. 最后可以进行 return, 值作为最后的 value, done 的值变为 true.
生成器传递参数- next()
在调用 next() 函数时, 传递参数, 则此参数会作为上一个 yield 语句的返回值, 然后在下一个yield 间进行运算
也就是说, 我们是为本次的代码块提供了一个参数值进来.
const generator = foo(5) // 生成器对象 的 next() 也有返回值
// next() 也可以传递参数
console.log(generator.next())// 给第二个 next() 传参
console.log(generator.next(10) )
给第二段代码传参, 则先需要在 第一个段代码的 yield 的地方进行定义和接收, 然后在第二个 yield 后边处理
function* foo() {console.log('第一段代码: ', 100)const n = yield 100 * num // 这里接收console.log("第二代代码: ", 200)const count = yield 200 * n // 这里调用
}
同样的做法, 可以给第一个 (很少见) 或者第三个都进行这样的处理:
// 生成器-next() 传递参数function* foo(num) {console.log('函数开始执行...')console.log('第一段代码: ', 100)const n = yield 100 * numconsole.log("第二代代码: ", 200)const count = yield 200 * n console.log("第三段代码: ", 300)yield 300 * count console.log('函数执行结束')return 'over'
}const generator = foo(5) // 生成器对象 的 next() 也有返回值
// next() 也可以传递参数
console.log(generator.next())
console.log(generator.next(10) )
console.log(generator.next(20) )console.log(generator.next() )
函数开始执行...
第一段代码: 100
{ value: 500, done: false }第二代代码: 200
{ value: 2000, done: false }第三段代码: 300
{ value: 6000, done: false }函数执行结束
{ value: 'over', done: true }
生成器传递参数- return()
还有一种传参方式是通过 return() 函数, 它会直接终止生成器, 在当前代码块执行之前.
但这样的话就表示生成器函数的结束, 之后调用 next 则不会再继续生成值了.
// 生成器函数-return 函数function* foo(num) {console.log('函数开始执行...')console.log('第一段代码: ', 100)const n = yield 100 * numconsole.log("第二代代码: ", 200)const count = yield 200 * n console.log("第三段代码: ", 300)yield 300 * count console.log('函数执行结束')return 'over'
}const generator = foo(5) console.log(generator.next())
// 第二段代码, 执行 return
console.log(generator.return(222))console.log(generator.next(20) )
console.log(generator.next())
函数开始执行...
第一段代码: 100
{ value: 500, done: false }
{ value: 222, done: true }{ value: undefined, done: true }
{ value: undefined, done: true }
则在第二段的时候, 调用 "generator.return(222)" 相当于是在第一段代码执行结束后进行 return
function* foo(num) {console.log('函数开始执行...')console.log('第一段代码: ', 100)const n = yield 100 * num// 相当于在这里终止了生成器return 222// 因为 return 了, 则后面的不会再进行 yield 新值了console.log("第二代代码: ", 200)const count = yield 200 * n // ....}
生成器抛出异常 - throw()
除了可以给生成器函数内部传参外, 也可以给生成器函数内部抛出异常.
- 抛异常后, 可以在生成器函数中捕获异常
- 但在
catch语句中则不能再yield新值了, 但在 catch 语句外用 yield 中断语句执行可以
// 生成器函数-throw() 捕捉异常function* foo() {console.log('函数开始执行...')console.log('第一段代码: ', 100)try {yield 100 } catch (err) {console.log('捕捉到异常: ', err)}console.log("第二代代码: ", 200)const count = yield 200 console.log("第三段代码: ", 300)yield 300 * count console.log('函数执行结束')return 'over'
}const generator = foo() console.log(generator.next())
// 第二段代码, 执行 throw, 则会在第一段代码执行完后就异常了
console.log(generator.throw('err'))// 处理完异常后, 后续逻辑则正常运行
console.log(generator.next(20))
console.log(generator.next())
函数开始执行...第一段代码: 100
{ value: 100, done: false }
捕捉到异常: err第二代代码: 200
{ value: 200, done: false }第三段代码: 300
{ value: 6000, done: false }函数执行结束
{ value: 'over', done: true }
生成器 替代 迭代器使用
生成器是一种特殊的迭代器, 则在一些情况下可以是使用生成器来代替迭代器的.
// 生成器 代替 迭代器// 迭代器
function arrayIterator(arr) {let index = 0return {next: function() {if (index < arr.length) {return {done: false, value: arr[index++]}} else {return {done: true, value: undefined}}}}
}const names = ['youge', 'cj', 'yaya']
const namesIterator = arrayIterator(names)console.log(namesIterator.next())
console.log(namesIterator.next())
console.log(namesIterator.next())// 也可以用生成器替代function* arrayGenerator(arr) {for (const item of arr) {yield item // {value: xxx, done: false}}}const namesGenerator = arrayGenerator(names)console.log(namesGenerator.next())console.log(namesGenerator.next())
我们还可以使用 yield* 来生成一个可迭代对象.
这时候相当于是一个 yield语法糖, 会以此迭代这个可迭代对象, 每次迭代其中的一个值.
// 生成器 代替 迭代器 yield* 语法糖function* arrayGenerator(arr) {// yield* 可迭代对象yield* arr
}const names = ['youge', 'cj', 'yaya']
const namesGenerator = arrayGenerator(names)console.log(namesGenerator.next())console.log(namesGenerator.next())console.log(namesGenerator.next())console.log(namesGenerator.next())
{ value: 'youge', done: false }
{ value: 'cj', done: false }
{ value: 'yaya', done: false }
{ value: undefined, done: true }
来个小案例, 创建一个函数, 这个函数可以迭代一个范围内的数字
// 用生成器代替迭代器function* createRangeIterator(start, end) {let index = start while (index < end) {yield index++ }
}const rangeIterator = createRangeIterator(1, 3)
console.log(rangeIterator.next())
console.log(rangeIterator.next())
console.log(rangeIterator.next())// { value: 1, done: false }
// { value: 2, done: false }
// { value: undefined, done: true }
// { value: undefined, done: true }
同样的, 对于之前自定义类的迭代器, 也是可以改写为生成器的.
// 自定义类 - 实现可迭代功能class Classroom {constructor(id, floor, students) {this.id = id this.floor = floor this.students = students }entry(newStudent) {this.students.push(newStudent)}// 让对象可迭代, 添加 [Symbol.iterator] 方法[Symbol.iterator]() {let index = 0return {next: () => {if (index < this.students.length) {return { done: false, value: this.students[index++] }} else {return { done: true, value: undefined }}}}}
}
现在可以改成这样的:
// 自定义类 - 迭代逻辑, 用生成器替换class Classroom {constructor(id, floor, students) {this.id = id this.floor = floor this.students = students }entry(newStudent) {this.students.push(newStudent)}// 用生成器来实现 [Symbol.iterator] 方法*[Symbol.iterator]() {yield* this.students // 妙呀!}
}
// 测试一下
const classroom = new Classroom("301", "3楼", ['bob', 'jack'])
classroom.entry('cj')// 需求: 将教室对象都迭代出来
for (const item of classroom) {console.log(item)
}
bob
jack
cj
cj@m
在实际应用中, 能用生成器解决问题, 尽量用它, 就不用自己写迭代器的一通逻辑, 直接用这种大道至简的语法糖:
yield* 可迭代对象 // 1行顶10行
至此, 关于迭代器和生成器的基本使用部分就到这了. 整体操作难度不大, 但是概念特别多, 比如说迭代器, 迭代器对象, 可迭代对象, 生成器, 生成器函数, 生成器是特殊迭代器, 生成器可以在某些情况下替代迭代器, for ...of 的语法糖, yield* 可迭代对象等. 就还是细节很多, 理解为主, 能用就行.
