本文翻译自Dr. Axel Rauschmayer的博客:http://www.2ality.com/2015/02/es6-iteration.html
本文是ES6中iteration的两篇博客:
Iterables and iterators in ECMAScript 6
ES6 generators in depth
ECMAScript 6对迭代/遍历(iteration)引入了一个新的接口: Iterable. 本文解释了它是如何工作,哪些语句通过它消费数据(例如for-of循环),哪些语句通过它提供数据源(例如arrays)。
iterability的思想如下:
数据消费者(Data consumers): JavaScript有一些语句消费数据。例如for-of 循环遍历 values,spread operator (...)把values插入arrays或function calls。
数据源(Data sources): 数据消费者可以从许多源头得到values。例如想要对一个array的元素遍历,对一个map中key-value entries的遍历,或者对一个string中characters的遍历。
让每个数据消费者支持所有的数据源并不现实,尤其是可能会不断创建新的数据源和消费者,例如通过数据结构或采用新的处理数据方式的库来创建。因此ES6引入了接口Iterable,数据消费者使用它,数据源实现它:
由于JavaScript没有接口概念,Iterable 更多的是个约定:
Source: 一个value被视为iterable ,当它有一个key为Symbol.iterator 的方法返回一个iterator。这个iterator是一个对象,通过其方法 next() 返回值,每调用一次next( )返回一个item,那我们就说它枚举items。
Consumption: 数据消费者使用iterator来获取消费的values。
我们来看如何消费一个array arr。首先通过key为Symbol.iterator的方法创建一个iterator:
> let arr = ['a', 'b', 'c']; > let iter = arr[Symbol.iterator]();
然后重复调用iterator的方法next()得到array内部的items:
> iter.next() { value: 'a', done: false } > iter.next() { value: 'b', done: false } > iter.next() { value: 'c', done: false } > iter.next() { value: undefined, done: true }
next() 返回的每一项被封装在一个对象中, 该对象的属性value 对应 item 的value,布尔值属性done 表示是否到items序列末尾。
Iterable与iterators是遍历(iteration)的协议(使用遍历的方法和规则)。该协议的一个关键特点是它是序列化的:iterator一次返回一个值。这意味着如果一个iterable数据结构不是线性的(例如是一个树), 那么遍历将使之线性化。
下面使用for-of 循环(后面还会详细解释)来遍历各种iterable数据。
Arrays (以及typed arrays)可以对其元素来遍历:
for (let x of ['a', 'b']) { console.log(x); } // Output: // 'a' // 'b'
Strings是可遍历的,但它们枚举的是Unicode编码点,每个Unicode编码点由1个或2个JavaScript “characters”组成:
for (let x of 'a\uD83D\uDC0A') { console.log(x); } // Output: // 'a' // '\uD83D\uDC0A' (crocodile emoji)
注意你已经看到了primitive values也是可遍历的。一个值不必是个对象才能遍历。
Maps [3] 可以对其entries遍历。每个entry被编码为一个[key, value] pair,由两个元素组成的一个数组。这些entries总是可以被一一枚举,与它们被插入到map中的顺序相同。
let map = new Map().set('a', 1).set('b', 2); for (let pair of map) { console.log(pair); } // Output: // ['a', 1] // ['b', 2]
注意WeakMaps [3]不可以遍历。
Sets [3] 可以对其entries遍历。它们被枚举的顺序与插入到set中的顺序相同。
let set = new Set().add('a').add('b'); for (let x of set) { console.log(x); } // Output: // 'a' // 'b'
注意WeakSets [3] 不可以遍历。
虽然特殊变量arguments 在ECMAScript 6中要被废弃 (由于rest parameters), 但它是可遍历的:
function printArgs() { for (let x of arguments) { console.log(x); } } printArgs('a', 'b'); // Output: // 'a' // 'b'
大多数DOM数据结构是可遍历的:
for (let node of document.querySelectorAll('···')) { ··· }
注意这个功能的实现还在开发中。但实现相对容易,因为symbol Symbol.iterator 不能与现有的property keys [2]相抵触。
并非所有iterable的内容都来自于数据结构,也可以是计算中得出的。例如所有的major ES6数据结构(arrays, typed arrays, maps, sets)都有三个方法返回iterable对象:
entries() 返回一个可遍历的entries,编码为[key,value] arrays. 对于arrays, values是数组元素,keys是索引。对于sets, 每个key和value相等,都等于set元素。
keys() 返回一个可遍历的 entries 的keys。
values() 返回一个可遍历的 entries 的values。
下面看看. entries() 如何给出array元素及其索引:
let arr = ['a', 'b', 'c']; for (let pair of arr.entries()) { console.log(pair); } // Output: // [0, 'a'] // [1, 'b'] // [2, 'c']
Plain objects (由object literals创建)不可以遍历:
for (let x of {}) { // TypeError console.log(x); }
理由如下。下面两个活动不同:
检查一个程序的结构(reflection)
遍历数据
最好保持这两个活动分开。#1与所有的对象相关,#2仅与数据结构相关。可以向对象Object.prototype添加一个方法[Symbol.iterator]()来使对象可遍历,但在两种情况下会无效:
如果是通过 Object.create(null) 创建,那么 Object.prototype 不在对象的原型链中。
如果它们是数据结构,那么就需要遍历数据。不仅不能对properties遍历,而且不能添加iterability到已有的类中,因为这将破坏对实例属性遍历的代码。
因此,使properties可遍历最安全的方式就是通过一个工具函数。例如通过objectEntries(), 其实现见后面 (未来的ECMAScript版本可能会内置类似的实现):
let obj = { first: 'Jane', last: 'Doe' }; for (let [key,value] of objectEntries(obj)) { console.log(`${key}: ${value}`); } // Output: // first: Jane // last: Doe
而且,重要的是记住针对对象properties的遍历主要当对象是maps [4]才有意义。但这仅在ES5中才这样做,因为别无他法。在ECMAScript 6中有Map.
本章节列出ES6中内置的所有使用了iteration协议的编程结构。
通过array来分解结构(Destructuring [5])模式可针对任意iterable:
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'];
for-of 是ECMAScript 6中的新引入的一个循环. 它的一种用法是:
for (let x of iterable) { ··· }
这个循环遍历iterable, 将每个枚举项赋值给遍历变量 x ,然后在循环体内处理。x的范围是循环内,出了循环不再存在。
注意iterable 需要能够遍历,否则for-of 不能做循环。这就意味着不可遍历的值必须转换为其它可遍历的。例如通过Array.from(), 可以将类似于array的值和iterables变为arrays:
let arrayLike = { length: 2, 0: 'a', 1: 'b' }; for (let x of arrayLike) { // TypeError console.log(x); } for (let x of Array.from(arrayLike)) { // OK console.log(x); }
我期待for-of最好能够替换Array.prototype.forEach(),因为它更为通用,forEach() 只能用于类似于array的值,而且对于很长的项for-of将更快(参见末尾的FAQ)。
如果用let来声明遍历的变量,那么对每个遍历将创建一个新的绑定(slot)。从下面的代码片段可以看出,通过一个箭头函数将当前的绑定elem保存起来以便后续使用。之后,会看到箭头函数没有共享同一个绑定elem, 每个有不同的elem。
let arr = []; for (let elem of [0, 1, 2]) { arr.push(() => elem); // save `elem` for later } console.log(arr.map(f => f())); // [0, 1, 2] // `elem`仅存在于循环内部: console.log(elem); // ReferenceError: elem is not defined
如果循环中使用var来声明遍历的变量看看发生了什么。现在所有的箭头函数指向同一个绑定elem。
let arr = []; for (var elem of [0, 1, 2]) { arr.push(() => elem); } console.log(arr.map(f => f())); // [2, 2, 2] // `elem` exists in the surrounding function: console.log(elem); // 2
当通过循环创建函数(例如添加event listeners)时,每次遍历有一个绑定就非常有帮助。
对于for循环和for-in循环,如果用let来声明遍历的变量,那么每次遍历都会得到一个绑定。
先看看for循环中用let来声明遍历的遍历i:
let arr = []; for (let i=0; i<3; i++) { arr.push(() => i); } console.log(arr.map(f => f())); // [0, 1, 2] console.log(i); // ReferenceError: i is not defined
如果这里使用var来声明i,就会得到传统的行为:
let arr = []; for (var i=0; i<3; i++) { arr.push(() => i); } console.log(arr.map(f => f())); // [3, 3, 3] console.log(i); // 3
类似地,对于for-in循环,用let来声明遍历的遍历key会使得每次遍历得到一个绑定:
let arr = []; for (let key in ['a', 'b', 'c']) { arr.push(() => key); } console.log(arr.map(f => f())); // ['0', '1', '2'] console.log(key); // ReferenceError: key is not defined
用var来声明key只会得到一个绑定:
let arr = []; for (var key in ['a', 'b', 'c']) { arr.push(() => key); } console.log(arr.map(f => f())); // ['2', '2', '2'] console.log(key); // '2'
以上只看了for-of中使用一个声明了的变量来遍历,但还有其他几种形式。
可以用一个先定义的变量来遍历:
let x; for (x of ['a', 'b']) { console.log(x); }
也可以用一个对象属性来遍历:
let obj = {}; for (obj.prop of ['a', 'b']) { console.log(obj.prop); }
还可以用一个array元素来遍历:
let arr = []; for (arr[0] of ['a', 'b']) { console.log(arr[0]); }
for-of循环与解构组合在一起,用于遍历key-value对(编码为arrays)非常有用。 下面以maps为例:
let map = new Map().set(false, 'no').set(true, 'yes'); for (let [k,v] of map) { console.log(`key = ${k}, value = ${v}`); } // Output: // key = false, value = no // key = true, value = yes
Array.prototype.entries() 也返回一个可遍历key-value对的iterable:
let arr = ['a', 'b', 'c']; for (let [k,v] of arr.entries()) { console.log(`key = ${k}, value = ${v}`); } // Output: // key = 0, value = a // key = 1, value = b // key = 2, value = c
因此entries() 可以根据枚举项的位置不同来做不同处理:
/** Same as arr.join(', ') */ function toString(arr) { let result = ''; for (let [i,elem] of arr.entries()) { if (i > 0) { result += ', '; } result += String(elem); } return result; }
这个函数的调用如下:
> toString(['eeny', 'meeny', 'miny', 'moe']) 'eeny, meeny, miny, moe'
Array.from() [6] 将iterable和类似array的值转换为arrays.,也可以用于typed arrays.
> Array.from(new Map().set(false, 'no').set(true, 'yes')) [[false,'no'], [true,'yes']] > Array.from({ length: 2, 0: 'hello', 1: 'world' }) ['hello', 'world']
Array.from() 就像Array的子类一样 (继承类的方法) – 将iterables转换为子类实例。
spread操作符[5] 将一个iterable值插入到一个array中:
> let arr = ['b', 'c']; > ['a', ...arr, 'd'] ['a', 'b', 'c', 'd']
这就提供了一种将任意iterable转换为array的紧凑方法:
let arr = [...iterable];
spread操作符还可以将一个iterable变为函数/方法或构造函数的参数:
> Math.max(...[-1, 8, 3]) 8
map的构造函数将一个个由[键, 值]对组成的iterable变为map:
> let map = new Map([['uno', 'one'], ['dos', 'two']]); > map.get('uno') 'one' > map.get('dos') 'two'
set的构造函数将一个个由元素组成的iterable变为set:
> let set = new Set(['red', 'green', 'blue']); > set.has('red') true > set.has('yellow') false
WeakMap与WeakSet 的构造函数与上述类似。而且maps与sets本身就是iterable(WeakMaps与WeakSets不是),这意味着可以用它们的构造函数来克隆它们。
Promise.all()和Promise.race()接受iterables over promises [7]:
Promise.all(iterableOverPromises).then(···); Promise.race(iterableOverPromises).then(···);
yield* [8] 会对一个iterable所有的枚举项让步.
function* yieldAllValuesOf(iterable) { yield* iterable; }
yield*最重要的一个用法是递归调用一个generator [8] (这将产生某种iterable).
遍历协议类似于下面这样:
如果一个对象(拥有或继承)有key为Symbol.iterator的方法,那么该对象就是可遍历对象iterable (即实现Iterable接口) ,这个方法必须返回一个iterator, 通过其方法next()来枚举可遍历对象内部的每一项。
在TypeScript中,iterables和iterators的接口类似于下面这样(参见 [9]):
interface Iterable { [System.iterator]() : Iterator; } interface IteratorResult { value: any; done: boolean; } interface Iterator { next(value? : any) : IteratorResult; return?() : IteratorResult; }
return 是一个可选方法,在后面介绍 (throw()也是可选方法,但实际上也从不用于iterators,将在下一篇博客中讨论a follow-up blog post on generators). 首先实现一个dummy iterable来感受下iteration是如何工作的。
let iterable = { [Symbol.iterator]() { let step = 0; let iterator = { next() { if (step <= 2) { step++; } switch (step) { case 1: return { value: 'hello', done: false }; case 2: return { value: 'world', done: false }; default: return { value: undefined, done: true }; } } }; return iterator; } };
现在来检查iterable:
for (let x of iterable) { console.log(x); } // Output: // hello // world
上面代码执行三步,用计数器step 来确保每一步输出对应的值。首先返回'hello',接下来返回'world',然后枚举项遍历结束。每一项都被封装在有下面属性的对象中:
value 对应实际的值
done 是个布尔标志, 表示是否遍历结束。
如果done 取值为 false或者value 是undefined则可以忽略。重写上面的 switch 语句:
switch (step) { case 1: return { value: 'hello' }; case 2: return { value: 'world' }; default: return { done: true }; }
在下一篇博客中会解释 the follow-up blog post on generators, 有些情况下希望done为true 时最后一项可以返回一个value. 否则的话,可以更简化next() 的实现,直接返回items而不用封装在对象中。可通过一个特殊值(例如一个符号symbol)来表示遍历结束。
下面再看一个iterable的实现,函数iterateOver() 返回一个对传入参数的遍历:
function iterateOver(...args) { let index = 0; let iterable = { [Symbol.iterator]() { let iterator = { next() { if (index < args.length) { return { value: args[index++] }; } else { return { done: true }; } } }; return iterator; } } return iterable; } // Using `iterateOver()`: for (let x of iterateOver('fee', 'fi', 'fo', 'fum')) { console.log(x); } // Output: // fee // fi // fo // fum
如果iterable和iterator是同一个对象,那么前面的函数可以简化为:
function iterateOver(...args) { let index = 0; let iterable = { [Symbol.iterator]() { return this; }, next() { if (index < args.length) { return { value: args[index++] }; } else { return { done: true }; } }, }; return iterable; }
即使最初的iterable和iterator不是同一个对象,但如果iterator有下面方法(这也使得iterator是可遍历的),那么有时候也有用处:
[Symbol.iterator]() { return this; }
ES6中所有内置的iterators都遵循这个模式(通过一个公共prototype, 参见follow-up blog post on generators). 例如,arrays的缺省iterator:
> let arr = []; > let iterator = arr[Symbol.iterator](); > iterator[Symbol.iterator]() === iterator true
为什么当一个iterator是可遍历的(即是一个iterable)有用呢? for-of 仅用于iterables, 而不能用于iterators. 正是由于array iterators是iterable, 因此可在另一个循环中继续遍历:
let arr = ['a', 'b']; let iterator = arr[Symbol.iterator](); for (let x of iterator) { console.log(x); // a break; } // Continue with same iterator: for (let x of iterator) { console.log(x); // b }
另一种方式就是用方法返回一个iterable。例如Array.prototype.values() 返回结果与缺省遍历方式相同。因此前面的代码段等同于下面代码:
let arr = ['a', 'b']; let iterable = arr.values(); for (let x of iterable) { console.log(x); // a break; } for (let x of iterable) { console.log(x); // b }
但使用iterable时,如果 for-of 调用了方法[Symbol.iterator]()则不能确保不会重新开始遍历。例如Array 的实例是iterables,当调用这个方法时将从头开始遍历。
继续遍历的一个用况是在通过 for-of 处理实际内容前先去掉初始项(例如一个header)。
两个iterator方法是可选的:
return() 当遍历过早终止时让iterator有机会清理残局。
throw() 转发方法调用给一个通过yield*来遍历的generator,详细解释见the follow-up blog post on generators.
前面提到,可选的iterator方法return() 就是还没有遍历结束时就让iterator停止,即关闭iterator。在for-of循环中,会由于以下原因过早终止premature(在语言规范中称中断abrupt) :
break
continue (如果在外部循环中继续遍历, continue行为类似于break)
throw
return
在上面四种情况下,for-of让iterator知道循环没有结束。下面看一个例子,函数readLinesSync 返回对一个文件每一文本行的遍历,不论发生什么都会关闭文件:
function readLinesSync(fileName) { let file = ···; return { ··· next() { if (file.isAtEndOfFile()) { file.close(); return { done: true }; } ··· }, return() { file.close(); return { done: true }; }, }; }
由于return(), 在下面循环中文件将被关闭:
// Only print first line for (let line of readLinesSync(fileName)) { console.log(x); break; }
return() 方法必须返回一个对象,这是由于generators处理return语句的方式,具体将在the follow-up blog post on generators解释。
下面的constructs会关闭还没有遍历到结尾的iterators:
for-of
yield*
Destructuring
Array.from()
Map(), Set(), WeakMap(), WeakSet()
Promise.all(), Promise.race()
在这一章节中来看更多iterables例子。这些iterables大多数通过generators更容易实现,详见The follow-up blog post on generators。
返回iterables的工具函数和方法就像iterable数据结构一样重要。下面是一个遍历对象属性的工具函数:
function objectEntries(obj) { let index = 0; // In ES6, you can use strings or symbols as property keys, // Reflect.ownKeys() retrieves both let propKeys = Reflect.ownKeys(obj); return { [Symbol.iterator]() { return this; }, next() { if (index < propKeys.length) { let key = propKeys[index]; index++; return { value: [key, obj[key]] }; } else { return { done: true }; } } }; } let obj = { first: 'Jane', last: 'Doe' }; for (let [key,value] of objectEntries(obj)) { console.log(`${key}: ${value}`); } // Output: // first: Jane // last: Doe
Combinators [10] 是组合已有iterables并创建新的iterable的函数。
先来看一个combinator函数take(n, iterable), 它返回一个iterable,由iterable前面 n 项组成。
function take(n, iterable) { let iter = iterable[Symbol.iterator](); return { [Symbol.iterator]() { return this; }, next() { if (n > 0) { n--; return iter.next(); } else { return { done: true }; } } }; } let arr = ['a', 'b', 'c', 'd']; for (let x of take(2, arr)) { console.log(x); } // Output: // a // b
zip 将n 个iterables组合为一个由n元数组(每个元数组tuple的编码是一个长度为n的数组)组成的iterable。
function zip(...iterables) { let iterators = iterables.map(i => i[Symbol.iterator]()); let done = false; return { [Symbol.iterator]() { return this; }, next() { if (!done) { let items = iterators.map(i => i.next()); done = items.some(item => item.done); if (!done) { return { value: items.map(i => i.value) }; } // Done for the first time: close all iterators for (let iterator of iterators) { iterator.return(); } } // We are done return { done: true }; } } }
可以看出,最短的iterable决定最终长度:
let zipped = zip(['a', 'b', 'c'], ['d', 'e', 'f', 'g']); for (let x of zipped) { console.log(x); } // Output: // ['a', 'd'] // ['b', 'e'] // ['c', 'f']
有些iterable可能永远不会结束done:
function naturalNumbers() { let n = 0; return { [Symbol.iterator]() { return this; }, next() { return { value: n++ }; } } }
对无穷尽iterable,一定不能遍历所有元素。例如可从一个for-of循环跳出:
for (let x of naturalNumbers()) { if (x > 2) break; console.log(x); }
或者仅访问无穷尽iterable的前面几项:
let [a, b, c] = naturalNumbers(); // a=0; b=1; c=2;
或者使用一个combinator函数。例如可以使用take()函数:
for (let x of take(3, naturalNumbers())) { console.log(x); } // Output: // 0 // 1 // 2
由zip() 返回的iterable的长度是由最短的iterable决定,即zip()和naturalNumbers() 就可以返回任意长度的iterable,包括无穷尽长度:
let zipped = zip(['a', 'b', 'c'], naturalNumbers()); for (let x of zipped) { console.log(x); } // Output: // ['a', 0] // ['b', 1] // ['c', 2]
你可能担心遍历协议很慢,因为每次调用next()需要创建一个新对象。但内存管理小对象对于现代引擎以及长时间运行是很快的,引擎可以优化遍历不需要为中间对象分配空间。更多信息可参见thread on es-discuss。
这篇博文虽然只涉及了ES6 iteration的基础,但内容已经很多了。Generators [8]以iterators的实现为基础。
JavaScript运行时库还没有与iterators工作的工具,Python有特性丰富的模块itertools, JavaScript最终也会有类似的模块。
“Exploring ES6: Upgrade to the next version of JavaScript”, book by Axel
Symbols in ECMAScript 6
ECMAScript 6: maps and sets
“Pitfalls: Using an Object as a Map” in “Speaking JavaScript”
Destructuring and parameter handling in ECMAScript 6 [includes an explanation of the spread operator (...)]
ECMAScript 6’s new array methods
ECMAScript 6 promises (2/2): the API
ES6 generators in depth
“Closing iterators”, slides by David Herman
“Combinator” in HaskellWiki