07-13 3422人
迭代器
迭代器是一种有序、连续的、基于拉取的用于消耗数据的组织方式,用于以一次一步的方式控制行为。
简单的来说我们迭代循环一个可迭代对象,不是一次返回所有数据,而是调用相关方法分次进行返回。
迭代器是帮助我们对某个数据结构进行遍历的对象,这个object
有一个next
函数,该函数返回一个有value
和done
属性的object
,其中value
指向迭代序列中当前next
函数定义的值。
{
done: boolean, // 为 true 时代表迭代完毕
value: any // done 为 true 时取值为 undefined
}
迭代协议
ES6的迭代协议分为迭代器协议(iterator protocol)和可迭代协议(iterable protocol),迭代器基于这两个协议进行实现。
迭代器协议: iterator协议
定义了产生value
序列的一种标准方法。只要实现符合要求的next
函数,该对象就是一个迭代器。相当遍历数据结构元素的指针,类似数据库中的游标。
可迭代协议: 一旦支持可迭代协议,意味着该对象可以用for-of
来遍历,可以用来定义或者定制 JS 对象的迭代行为。常见的内建类型比如Array
& Map
都是支持可迭代协议的。对象必须实现@@iterator
方法,意味着对象必须有一个带有@@iterator key
的可以通过常量Symbol.iterator
访问到的属性。
模拟实现一个迭代器
基于迭代器协议
// 实现
function createArrayIterator(arr) {
let index = 0
return {
next: () =>
index < arr.length
? { value: arr[index++], done: false }
: { value: undefined, done: true },
}
}
// 测试
const nums = [11, 22, 33, 44]
const numsIterator = createArrayIterator(nums)
console.log(numsIterator.next())
console.log(numsIterator.next())
console.log(numsIterator.next())
console.log(numsIterator.next())
console.log(numsIterator.next())
基于可迭代协议
实现了生成迭代器方法的对象称为 可迭代对象
也就是说这个对象中包含一个方法, 该方法返回一个迭代器对象
一般使用 Symbol.iterator
来定义该属性, 学名叫做 @@iterator
方法
// 一个可迭代对象需要具有[Symbol.iterator]方法,并且这个方法返回一个迭代器
const obj = {
names: ['111', '222', '333'],
[Symbol.iterator]() {
let index = 0
return {
next: () =>
index < this.names.length
? { value: this.names[index++], done: false }
: { value: undefined, done: true },
return: () => {
console.log('迭代器提前终止了...')
return { value: undefined, done: true }
},
}
},
}
// 测试
for (const item of obj) {
console.log(item)
if (item === '222') break
}
在上面两个模拟迭代器示例中,还是相对比较复杂,但是ES6引入了一个生成器对象,它可以让创建迭代器对象的过程变得简单很多。
生成器
生成器(Generator
)是一种返回 迭代器 的 函数,通过function
关键字后星号(*)来表示,函数中会用到新的关键字yield
。
// 生成器
function* creatIterator (){
yield 1
yield 2
yield 3
}
const iterator = creatIterator()
console.log(iterator.next()) // {value:1,done:false}
console.log(iterator.next()) // {value:2,done:false}
console.log(iterator.next()) // {value:3,done:false}
console.log(iterator.next()) // {value:undefined,done:true}
上述示例中,creatIterator()
前的星号* 表明它是一个生成器,通过yield
关键字来指定调用迭代器的next()
方法时的返回值和返回顺序。
每当执行完一条yield语句后函数就会自动停止执行。拿上面的例子来说,执行完语句yield 1
之后,函数便不再执行其他任何语言,直到再次调用迭代器的next()
方法才会继续执行 yield 2
语句。
注意:yield
表达式只能用在 Generator 函数里面,用在其他地方都会报错。
(function (){
yield 1;
})()
// SyntaxError: Unexpected number
注意:ES6 没有规定,function
关键字与函数名之间的星号,写在哪个位置。这导致下面的写法都能通过。
function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· }
function*foo(x, y) { ··· }
生成器传参
yield
表达式本身没有返回值,或者说总是返回undefined
。next
方法可以带一个参数,该参数就会被当作上一个yield
表达式的返回值。
function* dr(arg) {
console.log(arg)
let one = yield '111'
console.log(one)
yield '222'
console.log('ccc')
}
let iterator = dr('aaa')
console.log(iterator.next())
console.log(iterator.next('bbb'))
console.log(iterator.next())
应用场景
日常开发中会出现,下一个接口依赖于上一个接口的数据的情况,就可以使用生成器,而无需考虑异步回调地狱嵌套的问题。
模拟:1s后获取用户数据,2s后获取订单信息,3s后获取商品信息
function getUser() {
setTimeout(() => {
const data = '用户数据'
iterator.next(data)
}, 1000)
}
function getOrder() {
setTimeout(() => {
const data = '订单信息'
iterator.next(data)
}, 2000)
}
function getGoods() {
setTimeout(() => {
const data = '商品数据'
iterator.next(data)
}, 3000)
}
function* initData() {
const user = yield getUser()
console.log(user)
const order = yield getOrder()
console.log(order)
const goods = yield getGoods()
console.log(goods)
}
const iterator = initData()
iterator.next()
for of
for of
循环可以获取一对键值中的键值,因为这个循环和迭代器息息相关,就放在这里一起说了。
一个数据结构只要部署了Symbol.iterator
属性,就被视为具有iterator
接口,可以使用for of
,它可以循环可迭代对象。
JavaScript
默认有iterable
接口的数据结构:
- 数组Array
- Map
- Set
- String
- Arguments对象
- Nodelist对象,类数组 凡是部署了
iterator
接口的数据结构都可以使用数组的扩展运算符(…),和解构赋值等操作。
遍历数组
尝试用 for or 循环数组
既然数组是支持for...of
循环的,那数组肯定部署了 Iterator
接口,我们通过它来看看Iterator
的遍历过程。
从图中我们能看出:
Iterator
接口返回了一个有next
方法的对象。- 每调用一次 next,依次返回了数组中的项,直到它指向数据结构的结束位置。
- 返回的结果是一个对象,对象中包含了当前值
value
和 当前是否结束done
遍历对象
尝试遍历一下对象,我们会发现他报这个对象是不可迭代的,如下图
那我们可以使用上面的迭代器对象生成器让对象也支持for of
遍历
obj[Symbol.iterator] = function* () {
yield* this.name
}
也可以使用Object.keys()
获取对象的key
值集合,再使用for of
const obj = {name: 'youhun',age: 18}
for(const key of Object.keys(obj)){
console.log(key, obj[key])
// name youhun
// age 18
}
异步迭代
与同步可迭代对象部署了 [Symbol.iterator]
属性不同的是,异步可迭代对象的标志是部署了 [Symbol.asyncIterator]
这个属性。
// 用生成器生成
const obj = {
async *[Symbol.asyncIterator]() {
yield 1;
yield 2;
yield 3;
}
}
const asyncIterator = obj[Symbol.asyncIterator]()
asyncIterator.next().then(data => console.log(data)) // {value: 1, done: false}
asyncIterator.next().then(data => console.log(data)) // {value: 2, done: false}
asyncIterator.next().then(data => console.log(data)) // {value: 3, done: false}
asyncIterator.next().then(data => console.log(data)) // {value: undefined, done: true}
这里的 asyncIterator
就是异步迭代器了。与同步迭代器 iterator
不同的是,在 asyncIterator
上调用 next
方法得到是一个 Promise 对象,其内部值是 { value: xx, done: xx }
的形式,类似于 Promise.resolve({ value: xx, done: xx })
。
为什么要有异步迭代?
如果同步迭代器数据获取需要时间(比如实际场景中请求接口),那么再用 for-of
遍历的话,就有问题。
const obj = {
*[Symbol.iterator]() {
yield new Promise(resolve => setTimeout(() => resolve(1), 5000))
yield new Promise(resolve => setTimeout(() => resolve(2), 2000))
yield new Promise(resolve => setTimeout(() => resolve(3), 500))
}
}
console.log(Date.now())
for (const item of obj) {
item.then(data => console.log(Date.now(), data))
}
// 1579253648926
// 1579253649427 3 // 1579253649427 - 1579253648926 = 501
// 1579253650927 2 // 1579253650927 - 1579253648926 = 2001
// 1579253653927 1 // 1579253653927 - 1579253648926 = 5001
可以把这里的每个 item
当成是接口请求,数据返回的时间不一定的。上面的打印结果就说明了问题所在:我们控制不了数据的处理顺序。
再来看看异步迭代器
const obj = {
async *[Symbol.asyncIterator]() {
yield new Promise(resolve => setTimeout(() => resolve(1), 5000))
yield new Promise(resolve => setTimeout(() => resolve(2), 3000))
yield new Promise(resolve => setTimeout(() => resolve(3), 500))
}
}
console.log(Date.now())
for await (const item of obj) {
console.log(Date.now(), item)
}
// 1579256590699
// 1579256595700 1 // 1579256595700 - 1579256590699 = 5001
// 1579256598702 2 // 1579256598702 - 1579256590699 = 8003
// 1579256599203 3 // 1579256599203 - 1579256590699 = 8504
注意,异步迭代器要声明在 [Symbol.asyncIterator]
属性里,使用 for-await-of
循环处理的。最终效果是,对任务挨个处理,上一个任务等待处理完毕后,再进入下一个任务。
因此,异步迭代器就是用来处理这种不能即时拿到数据的情况,还能保证最终的处理顺序等于遍历顺序,不过需要依次排队等待。
for-await-of
我们可以使用如下代码进行遍历:
for await (const item of obj) {
console.log(item)
}
也就是说异步迭代遍历需要使用 for-await-of
语句。 除了能用在异步可迭代对象上,还能用在同步可迭代对象上。
const obj = {
*[Symbol.iterator]() {
yield 1
yield 2
yield 3
}
}
for await(const item of obj) {
console.log(item) // 1 -> 2 -> 3
}
注意:如果一个对象上同时部署了 [Symbol.asyncIterator]
和 [Symbol.iterator]
,那就会优先使用 [Symbol.asyncIterator]
生成的异步迭代器。这很好理解,因为 for-await-of
本来就是为异步迭代器而生的。
相反如果同时部署了两个迭代器,但使用的是for-or
那么优先使用同步迭代器。
const obj = {
*[Symbol.iterator]() {
yield 1
yield 2
yield 3
},
async *[Symbol.asyncIterator]() {
yield 4
yield 5
yield 6
}
}
// 异步
for await(const item of obj) {
console.log(item) // 4 -> 5 -> 6。优先使用由 [Symbol.asyncIterator] 生成的异步迭代器
}
// 同步
for (const item of obj) {
console.log(item) // 1 -> 2 -> 3。优先使用由 [Symbol.iterator] 生成的同步迭代器
}
总结
迭代器生成器逻辑可能有点绕,但是了解其原理是非常有必要的。可以自己尝试写一下,知其然知其所以然。这样才可以有需要的实现定义自己的迭代器来遍历对象,也可以应用在实际开发对应的场景中。
感谢分享
Windows 10 x64 Google Chrome 113.0.0.0