本文翻譯自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