一、基本概念
Iterator迭代器是一种接口,为不同的数据结构提供一种访问机制,即for … of 循环。当使用for…of循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口。任何数据结构只要部署Iterator接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
例如:
const arr = [1, 2, 3]
for (const number of arr) {
console.log(number)
}
二、实现原理
数组之所以能够支持for of 遍历,是因为ES6提前在数组中预置了一个接口:Symbol(Symbol.iterator)
其实for of是一个语法糖,它其实就是执行Symbol(Symbol.iterator)
接口来得到迭代器对象,然后通过这个迭代器对象获取到对应数据的的。
我们可以执行一下数组的迭代器接口看一下得到的是什么:
const arr = [1, 2, 3]
const iter = arr[Symbol.iterator]()
console.log(iter)
我们可以看到迭代器上有一个next方法,我们可以执行一下这个next方法:
console.log(iter.next())
console.log(iter.next())
console.log(iter.next())
console.log(iter.next())
我们可以看到,我们通过不断调用next()方法,就能获取所有的数据了。
结论:for of 循环只是提供了一个语法糖,通过不断调用迭代器接口提供的迭代器中的next方法来获取所有的数据
ES6规定,默认的迭代器接口都部署在数据结构的Symbol.iterator属性上,或者说,一个数据结构只要具有Symbol.iterator属性,那么它就是“可遍历的”,即可以使用for of 循环进行遍历。Symbol.iterator属性本身是一个函数,就是当前数据结构迭代器的生成函数,执行这个函数,就会得到一个遍历器。
原生具备Iterator接口的数据结构有:
- Array
- Set
- Map
- String
- arguments对象
- NodeList对象
- URLSearchParams对象 等等
三、手动实现
知道原理之后,我们可以尝试手动实现一个简单的遍历器
比如:如何给一个对象增加一个遍历器?
1.利用原有迭代器接口
const obj = {
0: '张三',
1: '李四',
2: '王二'
}
可以观察出这种对象是线性的,那么我们可以去利用数组的迭代器接口:
const obj = {
0: '张三',
1: '李四',
2: '王二',
length: 3,
[Symbol.iterator]: Array.prototype[Symbol.iterator]
}
for (const objElement of obj) {
console.log(objElement)
}
这样我们就借助数组的迭代器接口来实现了对象的遍历,当前,这种投机取巧的方式使用场景是很有限的,只是为了让大家更好理解迭代器接口。
2.手动实现
因为迭代器只支持线性遍历,所以手动实现的使用场景也不多,但是我们可以模拟一个场景:让一个类的私有属性支持循环遍历。
比如:
class ObjClass {
#list = ['a', 'b', 'c', 'd']
}
const obj = new ObjClass()
console.log(obj.#list) // Error
我们知道 #list现在是一个私有属性,我们希望外部去修改它,但是我们又希望可以支持外部进行遍历,这样我们就能使用手动实现迭代器的方式来做。
class ObjClass {
#list = ['a', 'b', 'c', 'd']; // 私有属性list
[Symbol.iterator]() {
let index = 0
// 返回迭代器对象
return {
next: () => {
if (index < this.#list.length) {
// 返回数据
return {
value: this.#list[index++], // 执行之后将index + 1
done: false
}
} else {
// 返回截止标识
return {
value: undefined,
done: true
}
}
}
}
}
}
const obj = new ObjClass()
// 直接遍历当前实例化对象,就能直接遍历私有属性 #list了
for (const listItem of obj) {
console.log(listItem)
}
四.其他使用场景
如果我们对刚才的实例化对象使用展开运算符的时候我们就会得到一个有趣的结果。
正常对对象展开时:
const objTest = {
a: '1',
b: '2'
}
console.log({...objTest})
手动添加迭代器接口的对象:
class ObjClass {
#list = ['a', 'b', 'c', 'd']; // 私有属性list
[Symbol.iterator]() {
let index = 0
// 返回迭代器对象
return {
next: () => {
if (index < this.#list.length) {
// 返回数据
return {
value: this.#list[index++], // 执行之后将index + 1
done: false
}
} else {
// 返回截止标识
return {
value: undefined,
done: true
}
}
}
}
}
}
const obj = new ObjClass()
console.log({...obj})
console.log([...obj])
这是因为我们调用 ...
展开运算符时都会默认先调用迭代器接口。所以如果我们手动添加了迭代器接口之后,执行展开运算符就会得到我们迭代器接口的值了。