可迭代协议
规定了某个对象是否可以迭代
一个对象本身或者原型链上有 Symbol.iterator 方法
当一个对象需要被迭代的时候(比如被置入一个 for...of 循环时),首先,会不带参数调用它的 @@iterator 方法,然后使用此方法返回的迭代器获得要迭代的值。
迭代协议
规定了某对象是否是迭代器
只有实现了一个拥有以下语义(semantic)的 **next()** 方法,一个对象才能成为迭代器:
迭代器
迭代器概念
所谓迭代器,其实就是一个具有 next() 方法的对象,每次调用 next() 都会按顺序返回数据结构的成员,该结果对象有两个属性,value 表示当前成员的值,done 表示遍历是否结束。
下面是一个模拟next方法返回值的例子。
var it = makeIterator(['a', 'b']);it.next() // { value: "a", done: false }it.next() // { value: "b", done: false }it.next() // { value: undefined, done: true }function makeIterator(array) {var nextIndex = 0;return {next: function() {return nextIndex < array.length ?{value: array[nextIndex++], done: false} :{value: undefined, done: true};}};}
调用指针对象的next方法,就可以遍历事先给定的数据结构。
没有对应数据解构的遍历起对象
var it = idMaker();it.next().value // 0it.next().value // 1it.next().value // 2// ...function idMaker() {var index = 0;return {next: function() {return {value: index++, done: false};}};}
默认迭代器接口
Iterator 接口的目的,就是为所有数据结构,提供了一种统一的访问机制,当使用for...of循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口。一种数据结构只要部署了 Iterator 接口,我们就称这种数据结构是“可遍历的”(iterable)。
默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。对于原生部署 Iterator 接口的数据结构,不用自己写遍历器生成函数,for...of循环会自动遍历它们。
原生具备 Iterator 接口的数据结构如下。
- Array
- Map
- Set
- String
- TypedArray
- 函数的 arguments 对象
- NodeList 对象
对象(Object)之所以没有默认部署 Iterator 接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。
本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。
严格地说,对象部署遍历器接口并不是很必要,因为这时对象实际上被当作 Map 结构使用,ES5 没有 Map 结构,而 ES6 原生提供了。
我们直接 for of 遍历一个对象,会报错,然而如果我们给该对象添加 Symbol.iterator 属性(原型链上的对象具有该方法也可):
var obj = {a: 1,b: 2,c: 3,[Symbol.iterator] : function () {let i = 0;return {next: () => {// debuggerreturn i < Object.values(this).length ?{value: Object.values(this)[i++]} : {done: true};}};}};var it = obj[Symbol.iterator]()it.next()it.next()for (let value of obj) {console.log(value)}// 1,2,3
class RangeIterator {constructor(start, stop) {this.value = start;this.stop = stop;}[Symbol.iterator]() { return this; }next() {var value = this.value;if (value < this.stop) {this.value++;return {done: false, value: value};}return {done: true, value: undefined};}}function range(start, stop) {return new RangeIterator(start, stop);}for (var value of range(0, 3)) {console.log(value); // 0, 1, 2}
上面代码是一个类部署 Iterator 接口的写法。Symbol.iterator属性对应一个函数,执行后返回当前对象的遍历器对象。
let iterable = {0: 'a',1: 'b',2: 'c',length: 3,[Symbol.iterator]: Array.prototype[Symbol.iterator]};for (let item of iterable) {console.log(item); // 'a', 'b', 'c'}
对于类似数组的对象(存在数值键名和length属性),部署 Iterator 接口,有一个简便方法,就是Symbol.iterator方法直接引用数组的 Iterator 接口。
注意,普通对象部署数组的Symbol.iterator方法,并无效果。
let iterable = {a: 'a',b: 'b',c: 'c',length: 3,[Symbol.iterator]: Array.prototype[Symbol.iterator]};for (let item of iterable) {console.log(item); // undefined, undefined, undefined}
使用 while 循环遍历
var $iterator = ITERABLE[Symbol.iterator]();var $result = $iterator.next();while (!$result.done) {var x = $result.value;// ...$result = $iterator.next();}
调用 Iterator 接口的场合
解构赋值
对数组和 Set 结构进行解构赋值时,会默认调用Symbol.iterator方法。
let set = new Set().add('a').add('b').add('c');let [x,y] = set;// x='a'; y='b'let [first, ...rest] = set;// first='a'; rest=['b','c'];
扩展运算符
扩展运算符(…)也会调用默认的 Iterator 接口。
// 例一var str = 'hello';[...str] // ['h','e','l','l','o']// 例二let arr = ['b', 'c'];['a', ...arr, 'd']// ['a', 'b', 'c', 'd']
yield*
yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。
let generator = function* () {yield 1;yield* [2,3,4];yield 5;};var iterator = generator();iterator.next() // { value: 1, done: false }iterator.next() // { value: 2, done: false }iterator.next() // { value: 3, done: false }iterator.next() // { value: 4, done: false }iterator.next() // { value: 5, done: false }iterator.next() // { value: undefined, done: true }
其他场合
由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口。下面是一些例子。
- for…of
- Array.from()
- Map(), Set(), WeakMap(), WeakSet()(比如
new Map([['a',1],['b',2]])) - Promise.all()
- Promise.race()
字符串的 Iterator 接口
是一个类似数组的对象,也原生具有 Iterator 接口。
上面代码中,调用var someString = "hi";typeof someString[Symbol.iterator]// "function"var iterator = someString[Symbol.iterator]();iterator.next() // { value: "h", done: false }iterator.next() // { value: "i", done: false }iterator.next() // { value: undefined, done: true }
Symbol.iterator方法返回一个遍历器对象,在这个遍历器上可以调用 next 方法,实现对于字符串的遍历。
可以覆盖原生的Symbol.iterator方法,达到修改遍历器行为的目的。
上面代码中,字符串 str 的var str = new String("hi");[...str] // ["h", "i"]str[Symbol.iterator] = function() {return {next: function() {if (this._first) {this._first = false;return { value: "bye", done: false };} else {return { done: true };}},_first: true};};[...str] // ["bye"]str // "hi"
Symbol.iterator方法被修改了,所以扩展运算符(...)返回的值变成了bye,而字符串本身还是hi。iIterator接口与 Generator 函数
Symbol.iterator方法的最简单实现,还是使用下一章要介绍的 Generator 函数。
var myIterable1 = {[Symbol.iterator]: function () {var nextIndex = 0;var array = [1,2,3]return {next: function () {return nextIndex < array.length ?{value: [1,2,3][nextIndex++],done: false} : {value: undefined,done: true}}}}}//var it = myIterable1[Symbol.iterator]();it.next()it.next()it.next()[...myIterable1]let myIterable = {[Symbol.iterator]: function* () {yield 1;yield 2;yield 3;}}[...myIterable] // [1, 2, 3]// 或者采用下面的简洁写法let obj = {* [Symbol.iterator]() {yield 'hello';yield 'world';}};for (let x of obj) {console.log(x);}// "hello"// "world"function *gen () {yield 1;yield 2;return 3}[...gen()]// 1 2
上面代码中,Symbol.iterator方法几乎不用部署任何代码,只要用 yield 命令给出每一步的返回值即可。
遍历器对象的 return(),throw()
遍历器对象除了具有next方法,还可以具有return方法和throw方法。如果你自己写遍历器对象生成函数,那么next方法是必须部署的,return方法和throw方法是否部署是可选的。return方法的使用场合是,如果for...of循环提前退出(通常是因为出错,或者有break语句),就会调用return方法。如果一个对象在完成遍历前,需要清理或释放资源,就可以部署return方法。
function readLinesSync(file) {return {[Symbol.iterator]() {return {next() {return { done: false };},return() {file.close();return { done: true };}};},};}
上面代码中,函数readLinesSync接受一个文件对象作为参数,返回一个遍历器对象,其r中除了next方法,还部署了return方法。下面的两种情况,都会触发执行return方法。
// 情况一for (let line of readLinesSync(fileName)) {console.log(line);break;}// 情况二for (let line of readLinesSync(fileName)) {console.log(line);throw new Error();}
上面代码中,情况一输出文件的第一行以后,就会执行return方法,关闭这个文件;情况二会在执行return方法关闭文件之后,再抛出错误。
注意,return方法必须返回一个对象,这是 Generator 规格决定的。throw方法主要是配合 Generator 函数使用,一般的遍历器对象用不到这个方法。
