51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

JS迭代器和生成器

背景 {#背景}

遍历数组的时候,我相信大多数人已经用上ES6的for...of语法了:

|---------------------|--------------------------------------------------------------------------------------| | 1 2 3 4 5 6 | const arr = [1, 2, 3] for (const item of arr) { console.log(item) } // 1 2 3 |

那么我们能不能让自己创建的对象也支持for...of呢? 答案自然是可以的,这么好用的语法不给一个自定义支持简直天理难容。

引入 {#引入}

在Python中,有一个好用的range对象,它可以让我们这样写代码:

|---------------------|-------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 | for i in range(5): print(i) ## 0 1 2 3 4 print(list(range(5))) ## [0, 1, 2, 3, 4] |

我们这里要借助迭代器和生成器实现的就是这个东西,实际上range在Python中也是通过迭代器的方式实现的。

同步迭代器 {#同步迭代器}

既然这么说了肯定有异步的迭代器,不过不着急,我们等会再讨论异步的问题,先上代码:

|---------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | function range(end) { let start = 0 const iterator = { next() { if (start === end) { return { done: true } } return { value: start++, done: false } } } return { [Symbol.iterator]: () => iterator } } for (const i of range(5)) { console.log(i) } // 0 1 2 3 4 |

我们定义了一个range函数,它返回一个可迭代对象

详细的内容请访问MDN查阅,我这里做一个总结:

  • 可迭代对象需要有一个Symbol.iterator函数属性,返回迭代器对象。
  • 迭代器对象需要有一个next函数属性,返回迭代结果。
  • 迭代结果需要有value属性和done属性,这个看名字就知道什么意思了。

所以说,根据这些,我们可以想到一个简化代码的思路,就是让range返回的对象本身也是一个迭代器,当调用Symbol.iterator时返回this

|------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | function range(end) { let start = 0 return { next() { if (start === end) { return { done: true } } return { value: start++, done: false } }, [Symbol.iterator]() { return this } } } for (const i of range(5)) { console.log(i) } // 0 1 2 3 4 |

已经优化了很多了,但是这样还是不够简练,于是引出我们下文要说的内容。

同步生成器 {#同步生成器}

我们可以发现只是实现一个简单的数字范围生成器就需要10多行代码,这显然是有点麻烦的,于是es6为我们提供了一个东西,它就是生成器

|------------------------------|--------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 | function* range(end) { for (let i = 0; i < end; i++) { yield i } } for (const i of range(5)) { console.log(i) } // 0 1 2 3 4 |

现在代码简单多了,然后我们看一下range函数返回了什么东西:

|-----------------------|--------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 | const r = range(5) console.log(r) // Object [Generator] {} const itor = r[Symbol.iterator]() console.log(r === itor) // true |

和我们自己实现的时候类似,它调用Symbol.iterator时返回的是this

相关语法 {#相关语法}

下面给出与迭代器和生成器有关的语法。

for...of语法 {#for…of语法}

遍历元素值:

|---------------------|---------------------------------------------------------------------------| | 1 2 3 4 5 6 | const arr = [1, 2] for (const i of arr) { console.log(i) } // 1 2 |

看到这里我们也可以大致模拟一下for...of内部的执行原理了:

|------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | function forOf(obj, action) { const itor = obj[Symbol.iterator]() while (true) { const res = itor.next() if (res.done) { break } action(res.value) } } forOf([1, 2, 3], console.log) // 1 2 3 |

for await...of语法 {#for-await…of语法}

在开发过程中,AJAX请求返回的Promise对象是很常见的。

我们用setTimeout模拟异步请求,有下列代码:

|---------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | function asyncValue(value) { return new Promise(resolve => { setTimeout(() => resolve(value), value * 100) }) } (async() => { const arr = [2, 4, 5].map(asyncValue) for await (const i of arr) { console.log(i) } // 2 4 5 })() |

for await...of语法可以来遍历一个Promise对象组成的数组。

展开语法 {#展开语法}

不管是迭代器生成器,都支持es6的展开语法:

|-------------------------|--------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 | function* values() { yield 1 yield 2 yield 3 } console.log([...values()]) // [ 1, 2, 3 ] |

不做过多的演示了,只需知道展开语法也利用了迭代器生成器即可。

异步迭代器 {#异步迭代器}

接下来,我们继续回到range对象上,上面的相关语法只是起一个过渡作用。

给对象一个Symbol.asyncIterator属性,支持它被for await...of迭代:

|---------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | function asyncValue(value) { return new Promise(resolve => { setTimeout(() => resolve(value), value * 100) }) } function asyncRange(end) { let start = 0 return { async next() { if (start === end) { return { done: true } } const value = await asyncValue(start++) return { value, done: false } }, [Symbol.asyncIterator]() { return this } } } (async() => { for await (const i of asyncRange(5)) { console.log(i) } // 0 1 2 3 4 })() |

异步生成器 {#异步生成器}

就是加一个async关键字:

|------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | function asyncValue(value) { return new Promise(resolve => { setTimeout(() => resolve(value), value * 100) }) } async function* asyncRange(end) { for (let i = 0; i < end; i++) { yield await asyncValue(i) } } (async() => { for await (const i of asyncRange(5)) { console.log(i) } // 0 1 2 3 4 })() |

赞(1)
未经允许不得转载:工具盒子 » JS迭代器和生成器