导语
遍历器 Iterator
是 ES6 为访问数据集合提供的统一接口。任何内部部署了遍历器接口的数据集合,对于用户来说,都可以使用相同方式获取到相应的数据结构。如果使用的是最新版 Chrome
浏览器,那么你要知道——我们所熟悉的数组小姐,已悄悄的打开了另一扇可抵达她心扉的小径。
1 正题
某个数据集合部署了 Iterator
接口,是指其 Symbol.iterator
属性指向一个能返回 Iterator
接口的函数。任何默认使用遍历器访问数据集合的方法,都会调用此属性以得到遍历器对象,再按照设定的顺序依次访问该数据结构的成员(关于 Symbol.iterator
请看最后一节的延伸阅读)。比如原生数组的遍历器为 [][Symbol.iterator]
,也可以直接通过其构造函数的原型获取 Array.prototype[Symbol.iterator]
。
1.1 基本行为
调用 Iterator
接口会返回一个新的遍历器对象(指针对象)。 对象中必然有 next
方法,用于访问下一个数据成员。指针初始时指向当前数据结构的起始位置。
第一次调用对象的 next
方法,指针指向数据结构的第一个成员。 第二次调用对象的 next
方法,指针指向数据结构的第二个成员。 不断的调用对象的 next
方法,直到它指向数据结构的结束位置。
每次调用 next
方法,都会返回相同的数据结构: { value, done }
。 其中 value
表示当前指向成员的值,没有则为 undefined
。 其中 done
是一个布尔值,表示遍历是否结束,结束为 true
,否则 false
。
遍历器接口的标准十分简洁,不提供诸如:操作内部指针、判断是否有值等等方法。只需要一直不断的调用 next
方法,当 done
为 false
时获取当时的 value
, done
为 true
时停止即可。第一次接触遍历器的行为模式是在 2016 的冬天,那时底蕴不够鸡毛也没长全,理解不了简洁性的适用和强大。直到现在——在即将打包被迫离开公司的前夕才蓦然的醒觉。多么痛的领悟啊。
let iterator = [1, 2, 3][Symbol.iterator]();
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}
1.2 简单实现
面向不同的数据结构,有不同的遍历器实现方法,我们简单的实现下数组的遍历器方法。
let res = null;
let iterator = myIterator([3, 7]);
console.log( iterator.next() ); // {value: 3, done: false}
console.log( iterator.next() ); // {value: 7, done: false}
console.log( iterator.next() ); // {value: undefined, done: true}
function myIterator(array = []) {
let index = 0;
return {
next() {
return index < array.length
? { value: array[index++], done: false }
: { value: undefined, done: true };
}
};
}
1.3 return & throw
除了为遍历器对象部署 next
方法,还可以有 return
和 throw
方法。其中 return
方法会在提前退出 for of
循环时(通常是因为出错,或触发了 break
语句)被调用。而 throw
方法主要是配合 Generator
函数使用,一般的遍历器对象用不到这个方法,所以不予介绍。
let obj = {
[Symbol.iterator]() {
let index = 0;
let array = [1, 2, 3];
return {
next() {
return index < array.length
? { value: array[index++], done: false }
: { value: undefined, done: true };
},
return() {
console.log('Trigger return.');
return {};
}
};
}
};
for (let v of obj) {
console.log(v); // 打印出:1, 2, 3,没触发 return 函数。
}
for (let v of obj) {
if (v === 2) break;
console.log(v); // 打印出:1,之后触发 return 函数。
}
for (let v of obj) {
if (v === 3) break;
console.log(v); // 打印出:1, 2,之后触发 return 函数。
}
for (let v of obj) {
if (v === 4) break;
console.log(v); // 打印出:1, 2, 3,没触发 return 函数。
}
for (let v of obj) {
if (v === 2) throw Error('error');
console.log(v); // 打印出:1,之后触发 return 函数,并报错停止执行。
}
2 原生支持
2.1 默认持有遍历器
原生默认持有遍历器接口的数据结构有: 基本类型: Array
, Set
, Map
(四种基本数据集合: Array
, Object
, Set
和 Map
)。 类数组对象: arguments
, NodeList
, String
。
let iterator = '123'[Symbol.iterator]();
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}
遍历器与先前的遍历方法 一个数据集合拥有遍历器接口,并不意味着所有遍历它的方法都是使用此接口。实际上,只有 ES6 新增的几种方式和某些方法会使用,下面会有介绍。以数组来说,对其使用 for
和 for of
虽然可访问到相同的成员,但是实际的操作方式却不同。
// 改变数组默认的遍历器接口。
Array.prototype[Symbol.iterator] = function () {
let index = 0;
let array = this;
console.log('Use iterator');
return {
next() {
return index < array.length
? { value: array[index++], done: false }
: { value: undefined, done: true };
}
}
};
let arr = [1, 2];
for (let v of arr) {
console.log(v); // 打印出 Use iterator, 1, 2。
}
for (let i = 0; i < arr.length; i++) {
console.log(arr[i]); // 打印出 1, 2。
}
arr.forEach(d => {
console.log(d); // 打印出 1, 2。
});
对象没有默认的遍历器接口 为什么对象没有默认的遍历器接口?这要从两方面说明。一为遍历器是种线性处理结构,对于任何非线性的数据结构,部署了遍历器接口,就等于部署一种线性转换。二是对象本来就是一个无序的集合,如果希望其有序,可以使用 Map
代替。这即是各有其长,各安其职。屎壳郎如果不滚粪球而去采蜜,那,呃,花妹妹可能就遭殃咯。
自行生成的类数组对象(拥有 length
属性),不具备遍历器接口。这与 String
等原生类数组对象不同,毕竟人家是亲生的,一出生就含着金钥匙(也不怕误吞)。不过我们可以将数组的遍历器接口直接应用于自行生成的类数组对象,简单有效无副作用。
let obj = {
0: 'a',
1: 'b',
length: 2,
[Symbol.iterator]: Array.prototype[Symbol.iterator]
};
let iterator = obj[Symbol.iterator]();
console.log( iterator.next() ); // {value: "a", done: false}
console.log( iterator.next() ); // {value: "b", done: false}
console.log( iterator.next() ); // {value: undefined, done: true}
为对象添加遍历器接口,也不影响之前不使用遍历器的方法,比如 for in
, Object.keys
等等(两者不等同)。
let obj = {
0: 'a',
1: 'b',
length: 2,
[Symbol.iterator]: Array.prototype[Symbol.iterator]
};
console.log( Object.keys(obj) ); // ["0", "1", "length"]
for (let v of obj) {
console.log(v); // 依次打印出:"a", "b"。
}
for (let k in obj) {
console.log(k); // 依次打印出:"0", "1", "length"。
}
2.2 默认调用遍历器
for of for of
是专门用来消费遍历器的,其遍历的是键值( for in
遍历的是键名)。
for (let v of [1, 2, 3]) {
console.log(v); // 依次打印出:1, 2, 3。
}
扩展运算符 无论是解构赋值或扩展运算都是默认调用遍历器的。
let [...a] = [3, 2, 1]; // [3, 2, 1]
let b = [...[3, 2, 1]]; // [3, 2, 1]
yield* 在 Generator
函数中有 yield*
命令,如果其后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。
for (let v of G()) {
console.log(v); // 依次打印出:1, 2, 3, 4, 5
}
function* G() {
yield 1;
yield* [2,3,4];
yield 5;
}
其它场合 有些接受数组作为参数的函数,会默认使用数组的遍历器接口,所以也等同于默认调用。比如 Array.from()
, Promise.all()
。
延伸阅读
关于 ES6 的 Symbol
:
https://segmentfault.com/a/1190000015244917#articleHeader3
作者:wmaker
https://segmentfault.com/a/1190000015701263
感兴趣的小伙伴,可以关注公众号【grain先森】,回复关键词 “vue”,获取更多资料,更多关键词玩法期待你的探索~
网友评论