可迭代协议
可迭代协议允许 JavaScript 对象定义或定制它们的迭代行为,例如,在一个 [
for..of]结构中,哪些值可以被遍历到。一些内置类型同时是内置的可迭代对象,并且有默认的迭代行为,比如 [Array]或者 [Map],而其他内置类型则不是(比如 [Object])。要成为可迭代对象,该对象必须实现@@iterator方法,这意味着对象(或者它原型链上的某个对象)必须有一个键为@@iterator的属性,可通过常量 [Symbol.iterator] 访问该属性:
[Symbol.iterator]: 一个无参数的函数,其返回值为一个符合迭代器协议的对象。
迭代器协议
迭代器协议定义了产生一系列值(无论是有限个还是无限个)的标准方式,当值为有限个时,所有的值都被迭代完毕后,则会返回一个默认返回值。只有实现了一个拥有以下语义的 next() 方法,一个对象才能成为迭代器:
next():
无参数或者接受一个参数的函数,并返回符合 IteratorResult 接口的对象。如果在使用迭代器内置的语言特征(例如 for...of)时,得到一个非对象返回值(例如 false 或 undefined),将会抛出 TypeError("iterator.next() returned a non-object value")。
所有迭代器协议的方法(next()、return() 和 throw())都应返回实现 IteratorResult 接口的对象。它必须有以下属性:
done[可选]
为布尔值
- true:迭代器已将序列迭代完毕,这种情况下,
value是可选的,如果它依然存在,即为迭代结束之后的默认返回值。 - false:迭代器能够生成序列中的下一个值(等价于没有指定
done这个属性)
value[可选]
迭代器返回的任何 JavaScript 值。done 为 true 时可省略。
实际上,两者都不是严格要求的;如果返回没有任何属性的对象,则实际上等价于 { done: false, value: undefined }。
如果一个迭代器返回一个 done: true 的结果,则对任何 next() 的后续调用也返回 done: true
return(value)[可选]
无参数或者接受一个参数的函数,并返回符合 IteratorResult 接口的对象,其 value 通常等价于传递的 value,并且 done 等于 true。调用这个方法表明迭代器的调用者不打算调用更多的 next(),并且可以进行清理工作。
throw(exception)[可选]
无参数或者接受一个参数的函数,并返回符合 IteratorResult 接口的对象,通常 done 等于 true。调用这个方法表明迭代器的调用者监测到错误的状况,并且 exception 通常是一个 [Error] 实例。
迭代器(Iterator)
JavaScript 原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6 又添加了Map和Set。这样就有了四种数据集合,用户还可以组合使用它们,定义自己的数据结构,比如数组的成员是Map,Map的成员是对象。这样就需要一种统一的接口机制,来处理所有不同的数据结构。
Iterator就是这样一种机制。它是一种接口,具有特定结构的对象(其中包含一个 next 方法,用于返回集合中的下一个元素),为各种不同的数据结构提供统一的访问机制。它不是新的语法或新的内置对象,而一种协议( 迭代器协议),所有遵守这个协议的对象,即包含 next,调用 next 返回一个result{value,done}。都可以称之为迭代器,完成遍历操作。
Iterator 接口的目的,就是为所有数据结构,提供一种统一的访问机制,即for...of循环。当使用for...of循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口,调用数据结构的Symbol.iterator方法。。
一种数据结构只要部署了 Iterator 接口,我们就称这种数据结构是“可遍历的”,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”
迭代器工作原理
- 创建一个迭代器对象,通常通过调用集合对象的
Symbol.iterator方法来获取迭代器对象。 - 调用迭代器对象的
next方法,每次调用都会返回一个包含value和done两个属性的对象。value 表示集合中的一个元素。done 表示迭代是否已完成,如果为 true,则表示迭代结束;如果为 false,则表示还有更多元素可供遍历。
- 重复调用 next 方法,直到迭代结束。
内置可迭代对象
- Array
- Map
- Set
- String
- TypedArray
- 函数的 arguments 对象
- NodeList 对象
数组
数组原生具有迭代器接口,部署在数据的Symbol.iterator上,所以调用这个属性就可以获取一个迭代器对象,然后使用迭代器的 next 方法逐个访问数组的元素。
const arr = [1, 2, 3, 4, 5];
const iterator = arr[Symbol.iterator]();
let result = iterator.next();
while (!result.done) { //遍历器对象每次移动指针,都检查一下返回值的done属性,如果遍历还没结束,就移动遍历器对象的指针到下一步,不断循环
console.log(result.value); //1,2,3,4,5
result = iterator.next();
}
Map
Map原生具有迭代器接口,可直接使用for...of遍历,此处通过调用 Map 对象的 entries() 方法获取一个迭代器对象,然后使用迭代器的 next 方法逐个访问 Map 的键值对。
const map = new Map([["a", 1], ["b", 2], ["c", 3]]);
const iterator = map.entries();
let result = iterator.next();
while (!result.done) {
const [key, value] = result.value;
console.log(key, value); //a 1,b 2,c 3
result = iterator.next();
}
Set
Set原生具有迭代器接口,可直接使用for...of遍历,此处通过调用 Set 对象的 values() 方法,获取一个迭代器对象,然后使用迭代器的 next 方法逐个访问 Set 的元素。
const set = new Set([1, 2, 3, 4, 5]);
const iterator = set.values();
let result = iterator.next();
while (!result.done) {
console.log(result.value); //1,2,3,4,5
result = iterator.next();
}
对象
对于原生部署 Iterator 接口的数据结构,不用自己写遍历器生成函数,for...of循环会自动遍历它们。除此之外,其他数据结构(主要是对象)的 Iterator 接口,都需要自己在Symbol.iterator属性上面部署,这样才会被for...of循环遍历。
对象(Object)之所以没有默认部署 Iterator 接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。不过,严格地说,对象部署遍历器接口并不是很必要,因为这时对象实际上被当作 Map 结构使用,ES5 没有 Map 结构,而 ES6 原生提供了。
//通过将字面量对象的key转为数组,然后调用数组的迭代器遍历对象:
const obj = {
name: 'aye',
age: 18,
sex: '男'
}
const iterator = Object.keys(obj)[Symbol.iterator]();
let result = iterator.next();
while (!result.done) {
let key = result.value;
console.log(`${key}--${obj[key]}`); //name--aye,age--18,sex--男
result = iterator.next();
}
类数组对象:
const obj = {
0: 'a',
1: 'b',
2: 'c',
length: 3,
[Symbol.iterator]: Array.prototype[Symbol.iterator]
}
for (let v of obj) {
console.log(v); //a,b,c
}
//注意,普通对象部署数组的Symbol.iterator方法,并无效果。
通过生成器函数遍历普通对象:
const obj = {
name: 'aye',
age: 18,
}
function* entries(obj) {
for (let key of Object.keys(obj)) {
yield [key, obj[key]]
}
}
for (let [key, value] of entries(obj)) {
console.log(`${key}--${value}`) //name--aye age--18
}
类部署迭代器接口:
class Range{
constructor(start, end) {
this.start = start;
this.end = end;
}
[Symbol.iterator]() {
return this;
}
next() {
const value = this.start;
if (value < this.end) {
this.start++;
return {done: false, value};
}
return {done: true, value: undefined};
}
}
function r(start, end) {
return new Range(start, end);
}
for (const value of r(1, 5)) {
console.log(value); //1,2,3,4
}
自定义迭代器
const myIterable = {
data: [1, 2, 3, 4, 5],
[Symbol.iterator]() {
let index = 0;
return {
next: () => {
console.log(index) //0,1,2
if (index < this.data.length) {
return { value: this.data[index++], done: false };
} else {
return { value: undefined, done: true };
}
},
return: () => { //如果`for...of`循环提前退出(通常是因为出错,或者有break语句),就会调用return()方法
console.log('Closing')
return { value: undefined, done: true };
}
};
},
};
for (const item of myIterable) {
console.log(item); //1,2,3
if (item === 3) {
break; //Closing
}
}
会默认调用迭代器接口的场景
- 解构赋值
- 扩展运算符
- for...of
- yield *
- Promise.all()
- Promise.race()
- Map()
- Set()
- WeakMap()
- WeakSet()