在平常项目中,调试时在数组等可迭代对象上看到过下图显示的 Symbol.iterator
属性:
它是 js 内置的一个 Symbol 值,指向该对象的默认的迭代器方法。某个对象是否能用 for of
遍历,判断的依据就是该对象有没有部署 Symbol.iterator
接口。下面就来说说,到底什么是迭代器,什么又是可迭代对象。
迭代器
在 js 中,迭代器是一个实现了迭代器协议( Iterator protocol )的对象。通过迭代器这一接口机制,让用户可以对各种不同的容器对象(数组、链表、Set、Map 等)进行遍访。
迭代器协议
迭代器协议定义了产生一系列值的标准方式,在 js 中就是指一个特定的 next()
方法 —— 一个无参数或只接收 1 个参数的函数。next()
方法一般是不接收参数的,但后续文章会介绍的生成器,作为一种特殊的迭代器,它的 next()
方法就可以接收 1 个参数。next()
的返回值是一个拥有 2 个属性的对象,value
和 done
:
- value
迭代器返回的任何 JavaScript 值。done
为 true
时可省略,如果此时 value
依然存在,即为迭代结束之后的默认返回值。
- done
值为布尔类型,如果迭代器可以产生序列中的下一个值,则为 false
。如果迭代器已将序列迭代完毕,则为 true
。
迭代器的实现
现在我们就根据迭代器协议来实现一个迭代器,它无非就是一个拥有特定 next()
方法的对象而已:
// 例 1
const arr = [1, 2, 3]
let index = 0
const iterator = {
next() {
if (index < arr.length) {
return { value: arr[index++], done: false }
} else {
return { value: undefined, done: true }
}
}
}
例 1 的 iterator
就是一个迭代器,它可以帮助我们遍历数组 arr
:
// 例 1.1
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 }
可迭代对象
可迭代对象是实现了可迭代协议(Iterable protocol,注意这里是 Iterable,而迭代器协议是 Iterator )的对象。可迭代协议要求,可迭代对象必须实现一个叫做 @@iterator
的方法,我们可以使用 js 内建的常量 Symbol.iterator
来访问该方法,而这个方法的返回值,是一个迭代器。下面我们实现一个可迭代对象 iterableObj:
// 例 2
const iterableObj = {
arr: [1, 2, 3],
[Symbol.iterator]: function () {
let index = 0
return {
next: () => {
if (index < this.arr.length) {
return { value: this.arr[index++], done: false }
} else {
return { value: undefined, done: true }
}
}
}
}
}
这里需要注意下 next 方法中 this 的指向问题,第 8 行我们之所以获取得到 arr 的 length,是因为我们用了箭头函数定义的 next 方法,那么 this
指向的就是上层作用域,也就是 [Symbol.iterator] 指向的函数的作用域,而该函数我们是用 function 定义的,所以当其被 iterableObj 调用时,其中的 this
指向的就是 iterableObj,所以我们在 next 方法中可以通过 this
去获取 iterableObj 的 arr。
现在,每次对 iterableObj[Symbol.iterator]
进行调用,得到的都是一个新的迭代器,可以通过迭代器的 next()
方法进行迭代:
// 例 2.1
const iterator1 = iterableObj[Symbol.iterator]()
console.log(iterator1.next()) // { value: 1, done: false }
console.log(iterator1.next()) // { value: 2, done: false }
console.log(iterator1.next()) // { value: 3, done: false }
console.log(iterator1.next()) // { value: undefined, done: true }
const iterator2 = iterableObj[Symbol.iterator]()
console.log(iterator2.next()) // { value: 1, done: false }
可迭代对象的应用
for of
我们知道普通的对象是不能用 for of
进行遍历的,因为一般的对象都不是可迭代对象,也就是它本身(或者它原型链上的某个对象)没有一个键为 @@iterator
的属性。而例 2 中我们自己的定义的 iterableObj,是一个可迭代对象,可以用 for of
遍历:
// 例 2.2
for (const iterator of iterableObj) {
console.log(iterator)
}
打印结果如下:
for of
本质上可以看成是一种语法糖,其背后是执行了像例 2.1 这样的代码,把执行 @@iterator
方法得到的迭代器每次调用 next()
得到的对象的 value 给返回出来。如果我们将例 2 第 8 行的 if 判断条件改为 if (index < this.arr.length - 1)
,那么例 2.2 的 for of
就会少遍历得到一个 3
。
js 中,Array,Map,Set,String,TypedArray,arguments,NodeList 对象等已经实现了可迭代协议,所以它们都可以使用 for of
进行遍历。我们自己也可以创建一个类,让这个类的实例是可迭代对象,其实实现的原理也是给这个类一个 [Symbol.iterator]
实例方法,让该方法返回一个迭代器:
// 例 2.2.3
class IterableClass {
constructor(arr) {
this.arr = arr
}
[Symbol.iterator]() {
let index = 0
return {
next: () => {
if (index < this.arr.length) {
return { value: this.arr[index++], done: false }
} else {
return { value: undefined, done: true }
}
},
return: () => {
console.log('监听到迭代提前终止')
return { done: true }
}
}
}
}
const iterable = new IterableClass([1, 2, 3])
for (const iterator of iterable) {
console.log(iterator)
if (iterator === 2) break
}
监听迭代提前终止
在例 2.2.3 的第 16 行我们还给迭代器写了个 return()
方法,用于监听像第 27 行发生的提前终止迭代的行为。return()
方法也需要返回一个拥有 value 和 done 属性的对象,由于 done 为 true,所以 value 可以省略。执行结果如下图:
展开语法
在调用函数或构造数组时使用的展开语法本质上也是用到了迭代器,所以只适用于可迭代对象。而 ES2018(ES9) 添加的在构造字面量对象时使用的展开语法,本质上并不是使用了迭代器,所以对象不能用 for of
但是可以使用展开语法。
解构数组
解构数组实际上也是通过迭代器,把数组里的值一个个通过 value 去获取:
// 例 3
const arr = [1, 2, 3]
const [a, b, c] = arr
console.log(a, b, c) // 1 2 3
但是 ES2018(ES9) 新添加的解构对象则不是。
接受可迭代对象作为参数的内置 API
诸如 new Map([iterable])
、Array.from(iterable)
和 Promise.all(iterable)
等,可参见 MDN 文档。