从 Iterator 迭代器到 Generator 生成器
迭代器的协议规范是:一个对象必须实现一个特定的接口,该接口包含一个名为 next
的方法,该方法返回一个对象,该对象包含两个属性:value
和 done
。
value
属性表示迭代器返回的当前值,done
属性是一个布尔值,表示迭代器是否已经迭代完所有元素。
{
value: any,
done: boolean
}
此外还需要实现 Symbol.iterator
方法,使得 for...of
/解构/扩展运算符 等可以遍历该对象。
若数据结构(对象)具备了 Symbol.iterator
属性,则它就是可迭代的,可以被迭代器遍历。并且可以用 for...of
遍历。以类数组举例,默认是不具备 Symbol.iterator
属性的,所以不能被迭代器遍历,也不能用 for...of
遍历。(数组这个属性在原先上)
常见可迭代对象
- Array
- Map
- Set
- String
- TypedArray
- arguments 对象
- NodeList 对象
虽然类数组对象不具备 Symbol.iterator
属性,但是它们可以通过 Array.from
方法转换为数组,然后使用 Symbol.iterator
属性进行迭代。此外部分类数组也是具备这个原型方法的, 也具有 length
属性。但是它们不是纯数组, 不能直接使用 map
、filter
等数组方法。如 arguments
、NodeList
、HTMLCollection
等。
// node 节点,所有类型节点(Element、Text、Comment...),例如通过 document.querySelector('*') 获取的节点。这个是静态快照,后续节点更改,这个NodeList 不会改变。
NodeList.prototype[Symbol.iterator]
// 元素集合, 仅包含元素节点(Element),例如通过 document.getElementsByClassName('*') 获取的元素集合。动态实时更新,DOM 变化时自动同步
HTMLCollection.prototype[Symbol.iterator]
js 简单实现
实现要点:
next
方法返回一个对象,该对象包含两个属性:value
和done
。- 当返回的
done
为true
时,表示迭代器已经迭代完所有元素。 - 需实现
Symbol.iterator
方法,使得for...of
/解构/扩展运算符 等可以遍历该对象。 - 可维护状态,通过
index
记录当前遍历的位置。
class Iterator {
constructor(items = []) {
this.items = items // 存储要遍历的元素
this.index = 0 // 当前索引
}
next() {
if (this.index < this.items.length) {
return {
value: this.items[this.index++],
done: false,
}
} else {
return {
value: undefined,
done: true,
}
}
}
// 为了支持 for...of 循环,我们需要在 Iterator 类中实现 Symbol.iterator 方法,该方法返回一个迭代器对象。迭代器对象需要实现 next 方法,该方法返回一个对象,包含 value 和 done 属性。value 是当前迭代的值,done 是一个布尔值,表示迭代是否结束。
[Symbol.iterator]() {
return this
}
}
const iterator = new Iterator([10, 20, 30])
console.log(iterator.next()) // { value: 10, done: false }
console.log(iterator.next()) // { value: 20, done: false }
console.log(iterator.next()) // { value: 30, done: false }
console.log(iterator.next()) // { value: undefined, done: true }
// 支持 for...of
for (const val of new Iterator(['a', 'b', 'c'])) {
console.log(val) // 输出 'a' 'b' 'c'
}
对象实现迭代
Object.prototype[Symbol.iterator] = function () {
// const keys = Object.keys(this)
const keys = Reflect.ownKeys(this) // 相较于 Object.keys,Reflect.ownKeys 可以获取到不可枚举属性和 Symbol 属性。
let index = 0 // 当前索引
return {
next: () => {
if (index < keys.length) {
return {
value: this[keys[index++]],
done: false,
}
} else {
return {
value: undefined,
done: true,
}
}
},
}
}
const obj = { a: 1, b: 2, c: 3, [Symbol('sym')]: 4 }
for (const val of obj) {
console.log(val) // 1, 2, 3, 4
}
关于 Reflect.ownKeys 的额外总结
方法 | 能否获取 Symbol key |
---|---|
Object.keys() | ❌ 只返回字符串 key |
Object.getOwnPropertyNames() | ❌ 只返回字符串 key |
for...in | ❌ 只枚举字符串、可枚举 key |
JSON.stringify() | ❌ 跳过 Symbol key |
Reflect.ownKeys() | ✅ 返回 所有 key (字符串 + Symbol) |
Object.getOwnPropertySymbols() | ✅ 只返回 Symbol key |
举例:
const obj = { a: 1, b: 2, c: 3, [Symbol('sym')]: 4 }
Object.keys(obj) // 只返回字符串 key: ['a', 'b', 'c']
for (let key in obj) console.log(key) // 只打印 'a', 'b', 'c'
JSON.stringify(obj) // {"a":1,"b":2,"c":3}
Reflect.ownKeys(obj) // ['a', 'b', 'c', Symbol(sym)]
Object.getOwnPropertySymbols(obj) // [Symbol(sym)]
迭代运行的时间
关于运行效率,这里对比一下 for、while、for...of、for...in、forEach、map 的运行时间。
const arr = new Array(1000000).fill(1)
console.time('for')
for (let i = 0; i < arr.length; i++) {}
console.timeEnd('for') // for: 2.17ms
console.time('while')
let i = 0
while (i < arr.length) {
i++
}
console.timeEnd('while') // while: 1.141ms
console.time('for...of')
for (const val of arr) {
}
console.timeEnd('for...of') // for...of: 8.81ms
console.time('for...in')
for (const key in arr) {
}
console.timeEnd('for...in') // for...in: 87.847ms
console.time('forEach')
arr.forEach(() => {})
console.timeEnd('forEach') // forEach: 6.422ms
console.time('map')
arr.map(() => {})
console.timeEnd('map') // map: 7.874ms
可以看出 for...in
是最慢的,原因在于:for...in
会遍历所有可枚举属性,包括原型链上的可枚举属性,而其他方法只会遍历当前对象的属性。在实际开发中,因避免使用。
Generator 生成器
Generator 生成器函数式一个特殊的函数,它返回一个迭代器对象。生成器函数使用 function*
语法来定义,函数体内部使用 yield
关键字来定义迭代器的值。
箭头函数无法变为生成器函数
每一个生成器函数,都是 GeneratorFunction 这个类的实例。
fn.__proto__
===GeneratorFunction.prototype
Generator 返回的是迭代器对象,它本身不执行函数体。
function* foo() { console.log('start') yield 1 yield 2 console.log('end') } const iterator = foo() // 什么都不会打印, 此时状态为 foo {<suspended>} console.log(iterator.next()) // 执行到第一个 yield: start { value: 1, done: false } console.log(iterator.next()) // 执行到第二个 yield: { value: 2, done: false } console.log(iterator.next()) // 执行到函数末尾: end { value: undefined, done: true }
- 每次
.next()
,函数会「恢复」到上一次 yield 停止处,直到下一个 yield 或结束。 - 当遇到函数体中的
return
,则done
为true
,并且value
为return
的值。 - 如果没有遇到
yield
或return
,运行到函数结尾了,则done
为true
,value
为undefined
。
- 每次
yield 只能在 Generator 函数内部使用,否则会报错。
.next()
可传参数,作为上一个 yield 表达式的返回值。注意:第一次.next()
传参是无效的,只有从第二次开始生效。function* gen() { const a = yield 'first' console.log('a:', a) // 接手到外部传入值 yield 'second' } const it = gen() console.log(it.next()) // { value: 'first', done: false } console.log(it.next('hello')) // 打印 'x: hello'
Generator 可以手动提前终止,利用 return 和 throw。
// return function* gen() { yield 1 console.log('1') // return 会终止迭代器, 不会执行到这里 yield 2 console.log('2') } const it = gen() console.log(it.next()) // { value: 1, done: false } console.log(it.return('hello')) // { value: 'hello', done: true } console.log(it.next()) // { value: undefined, done: true}
// throw function* gen() { try { yield 1 } catch (e) { console.log('Caught:', e) } } const it = gen() console.log('it:', it.next()) // { value: 1, done: false } const oops = it.throw(new Error('Oops')) // Caught: Error: Oops console.log('oops:', oops) // { value: undefined, done: true } console.log('it:', it.next()) // { value: undefined, done: true }
Generator 不会自动递归嵌套调用,如果
yield
另一个Generator
,它不会自动遍历里面的值!需要使用yield*
语法进行委托。function* inner() { yield 'inner1' yield 'inner2' } function* outer() { yield 'outer' yield inner() // 只是 yield 一个 Generator 对象 } for (const v of outer()) { console.log(v) // 输出: 'outer' 和 [object Generator] }
用 yield* 语法进行委托:
function* outer() { yield 'outer' yield* inner() // 展开 inner 的 yield }
每次生成的迭代器对象都是独立的,互不影响。
function* foo() { yield 1 } const g1 = foo() const g2 = foo() g1.next() // { value: 1, done: false } g2.next() // { value: 1, done: false } // 独立互不影响
Generator 模拟 async/await
Generator 自带暂停/恢复特性,非常适合实现 异步控制流,比如配合 Promise 和 co 库来实现同步写异步逻辑。
function* asyncGen() {
const res = yield fetchData()
console.log(res) // 这里的 res 是一个 Promise 对象
}
但注意 Generator 本身不是异步函数,它只是暂停函数执行,真正异步控制需要外部配合 Promise 实现。