本文翻译自Dr. Axel Rauschmayer的博客:http://www.2ality.com/2015/02/es6-iteration.html
本文是ES6中iteration的两篇博客:
ECMAScript 6对迭代/遍历(iteration)引入了一个新的接口: Iterable. 本文解释了它是如何工作,哪些语句通过它消费数据(例如for-of循环),哪些语句通过它提供数据源(例如arrays)。
Iterability
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数据结构不是线性的(例如是一个树), 那么遍历将使之线性化。
Iterable数据源
下面使用for-of 循环(后面还会详细解释)来遍历各种iterable数据。
Arrays
Arrays (以及typed arrays)可以对其元素来遍历:
for (let x of ['a', 'b']) {
console.log(x);
}
// Output:
// 'a'
// 'b'
Strings
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
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
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
虽然特殊变量arguments 在ECMAScript 6中要被废弃 (由于rest parameters), 但它是可遍历的:
function printArgs() {
for (let x of arguments) {
console.log(x);
}
}
printArgs('a', 'b');
// Output:
// 'a'
// 'b'
DOM数据结构
大多数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不可以遍历
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.
Iterating language constructs
本章节列出ES6中内置的所有使用了iteration协议的编程结构。
通过array来分解结构模式
通过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 循环
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 声明 vs. var 声明
如果用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循环和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()
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
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
Maps与sets
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不是),这意味着可以用它们的构造函数来克隆它们。
Promises
Promise.all()和Promise.race()接受iterables over promises [7]:
Promise.all(iterableOverPromises).then(···);
Promise.race(iterableOverPromises).then(···);
yield*
yield* [8] 会对一个iterable所有的枚举项让步.
function* yieldAllValuesOf(iterable) {
yield* iterable;
}
yield*最重要的一个用法是递归调用一个generator [8] (这将产生某种iterable).
实现iterables
遍历协议类似于下面这样:
如果一个对象(拥有或继承)有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
Iterators是可遍历的(iterable)
如果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() 与 throw()
两个iterator方法是可选的:
-
return() 当遍历过早终止时让iterator有机会清理残局。
-
throw() 转发方法调用给一个通过yield*来遍历的generator,详细解释见the follow-up blog post on generators.
通过return()关闭iterators
前面提到,可选的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例子。这些iterables大多数通过generators更容易实现,详见The follow-up blog post on generators。
返回iterables的工具函数
返回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
iterables组合函数
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']
无穷尽iterables
有些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]
常见问题(FAQ)
iteration协议慢吗?
你可能担心遍历协议很慢,因为每次调用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
-
“Pitfalls: Using an Object as a Map” in “Speaking JavaScript”
-
Destructuring and parameter handling in ECMAScript 6 [includes an explanation of the spread operator (...)]
-
“Closing iterators”, slides by David Herman
-
“Combinator” in HaskellWiki