本文翻译自Dr. Axel Rauschmayer的博客:http://www.2ality.com/2015/02/es6-iteration.html javascript
本文是ES6中iteration的两篇博客: html
ECMAScript 6对迭代/遍历(iteration)引入了一个新的接口: Iterable. 本文解释了它是如何工做,哪些语句经过它消费数据(例如for-of循环),哪些语句经过它提供数据源(例如arrays)。 python
iterability的思想以下: git
数据消费者(Data consumers): JavaScript有一些语句消费数据。例如for-of 循环遍历 values,spread operator (...)把values插入arrays或function calls。 es6
数据源(Data sources): 数据消费者能够从许多源头获得values。例如想要对一个array的元素遍历,对一个map中key-value entries的遍历,或者对一个string中characters的遍历。 github
让每一个数据消费者支持全部的数据源并不现实,尤为是可能会不断建立新的数据源和消费者,例如经过数据结构或采用新的处理数据方式的库来建立。所以ES6引入了接口Iterable,数据消费者使用它,数据源实现它: 编程
因为JavaScript没有接口概念,Iterable 更多的是个约定: api
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
“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