[1]引入 [2]迭代器 [3]生成器 [4]可迭代對象 [5]內建迭代器 [6]高級迭代器 [7]非同步任務執行 ...
前面的話
用迴圈語句迭代數據時,必須要初始化一個變數來記錄每一次迭代在數據集合中的位置,而在許多編程語言中,已經開始通過程式化的方式用迭代器對象返回迭代過程中集合的每一個元素
迭代器的使用可以極大地簡化數據操作,於是ES6也向JS中添加了這個迭代器特性。新的數組方法和新的集合類型(如Set集合與Map集合)都依賴迭代器的實現,這個新特性對於高效的數據處理而言是不可或缺的,在語言的其他特性中也都有迭代器的身影:新的for-of迴圈、展開運算符(...),甚至連非同步編程都可以使用迭代器
本文將詳細介紹ES6中的迭代器(Iterator)和生成器(Generator)
引入
下麵是一段標準的for迴圈代碼,通過變數i來跟蹤colors數組的索引,迴圈每次執行時,如果i小於數組長度len則加1,並執行下一次迴圈
var colors = ["red", "green", "blue"]; for (var i = 0, len = colors.length; i < len; i++) { console.log(colors[i]); }
雖然迴圈語句語法簡單,但如果將多個迴圈嵌套則需要追蹤多個變數,代碼複雜度會大大增加,一不小心就錯誤使用了其他for迴圈的跟蹤變數,從而導致程式出錯。迭代器的出現旨在消除這種複雜性並減少迴圈中的錯誤
迭代器
迭代器是一種特殊對象,它具有一些專門為迭代過程設計的專有介面,所有的迭代器對象都有一個next()方法,每次調用都返回一個結果對象。結果對象有兩個屬性:一個是value,表示下一個將要返回的值;另一個是done,它是一個布爾類型的值,當沒有更多可返回數據時返回true。迭代器還會保存一個內部指針,用來指向當前集合中值的位置,每調用一次next()方法,都會返回下一個可用的值
如果在最後一個值返回後再調用next()方法,那麼返回的對象中屬性done的值為true,屬性value則包含迭代器最終返回的值,這個返回值不是數據集的一部分,它與函數的返回值類似,是函數調用過程中最後一次給調用者傳遞信息的方法,如果沒有相關數據則返回undefined
下麵用ES5的語法創建一個迭代器
function createIterator(items) { var i = 0; return { next: function() { var done = (i >= items.length); var value = !done ? items[i++] : undefined; return { done: done, value: value }; } }; } var iterator = createIterator([1, 2, 3]); console.log(iterator.next()); // "{ value: 1, done: false }" console.log(iterator.next()); // "{ value: 2, done: false }" console.log(iterator.next()); // "{ value: 3, done: false }" console.log(iterator.next()); // "{ value: undefined, done: true }" // 之後的所有調用 console.log(iterator.next()); // "{ value: undefined, done: true }"
在上面這段代碼中,createIterator()方法返回的對象有一個next()方法,每次調用時,items數組的下一個值會作為value返回。當i為3時,done變為true;此時三元表達式會將value的值設置為undefined。最後兩次調用的結果與ES6迭代器的最終返回機制類似,當數據集被用盡後會返回最終的內容
上面這個示例很複雜,而在ES6中,迭代器的編寫規則也同樣複雜,但ES6同時還引入了一個生成器對象,它可以讓創建迭代器對象的過程變得更簡單
生成器
生成器是一種返回迭代器的函數,通過function關鍵字後的星號(*)來表示,函數中會用到新的關鍵字yield。星號可以緊挨著function關鍵字,也可以在中間添加一個空格
// 生成器 function *createIterator() { yield 1; yield 2; yield 3; } // 生成器能像正規函數那樣被調用,但會返回一個迭代器 let iterator = createIterator(); console.log(iterator.next().value); // 1 console.log(iterator.next().value); // 2 console.log(iterator.next().value); // 3
在這個示例中,createlterator()前的星號表明它是一個生成器;yield關鍵字也是ES6的新特性,可以通過它來指定調用迭代器的next()方法時的返回值及返回順序。生成迭代器後,連續3次調用它的next()方法返回3個不同的值,分別是1、2和3。生成器的調用過程與其他函數一樣,最終返回的是創建好的迭代器
生成器函數最有趣的部分是,每當執行完一條yield語句後函數就會自動停止執行。舉個例子,在上面這段代碼中,執行完語句yield 1之後,函數便不再執行其他任何語句,直到再次調用迭代器的next()方法才會繼續執行yield 2語句。生成器函數的這種中止函數執行的能力有很多有趣的應用
使用yield關鍵字可以返回任何值或表達式,所以可以通過生成器函數批量地給迭代器添加元素。例如,可以在迴圈中使用yield關鍵字
function *createIterator(items) { for (let i = 0; i < items.length; i++) { yield items[i]; } } let iterator = createIterator([1, 2, 3]); console.log(iterator.next()); // "{ value: 1, done: false }" console.log(iterator.next()); // "{ value: 2, done: false }" console.log(iterator.next()); // "{ value: 3, done: false }" console.log(iterator.next()); // "{ value: undefined, done: true }" // 之後的所有調用 console.log(iterator.next()); // "{ value: undefined, done: true }"
在此示例中,給生成器函數createlterator()傳入一個items數組,而在函數內部,for迴圈不斷從數組中生成新的元素放入迭代器中,每遇到一個yield語句迴圈都會停止;每次調用迭代器的next()方法,迴圈會繼續運行並執行下一條yield語句
生成器函數是ES6中的一個重要特性,可以將其用於所有支持函數使用的地方
【使用限制】
yield關鍵字只可在生成器內部使用,在其他地方使用會導致程式拋出錯誤
function *createIterator(items) { items.forEach(function(item) { // 語法錯誤 yield item + 1; }); }
從字面上看,yield關鍵字確實在createlterator()函數內部,但是它與return關鍵字一樣,二者都不能穿透函數邊界。嵌套函數中的return語句不能用作外部函數的返回語句,而此處嵌套函數中的yield語句會導致程式拋出語法錯誤
【生成器函數表達式】
也可以通過函數表達式來創建生成器,只需在function關鍵字和小括弧中間添加一個星號(*)即可
let createIterator = function *(items) { for (let i = 0; i < items.length; i++) { yield items[i]; } }; let iterator = createIterator([1, 2, 3]); console.log(iterator.next()); // "{ value: 1, done: false }" console.log(iterator.next()); // "{ value: 2, done: false }" console.log(iterator.next()); // "{ value: 3, done: false }" console.log(iterator.next()); // "{ value: undefined, done: true }" // 之後的所有調用 console.log(iterator.next()); // "{ value: undefined, done: true }"
在這段代碼中,createlterator()是一個生成器函數表達式,而不是一個函數聲明。由於函數表達式是匿名的,因此星號直接放在function關鍵字和小括弧之間。此外,這個示例基本與前例相同,使用的也是for迴圈
[註意]不能用箭頭函數來創建生成器
【生成器對象的方法】
由於生成器本身就是函數,因而可以將它們添加到對象中。例如,在ES5風格的對象字面量中,可以通過函數表達式來創建生成器
var o = { createIterator: function *(items) { for (let i = 0; i < items.length; i++) { yield items[i]; } } }; let iterator = o.createIterator([1, 2, 3]);
也可以用ES6的函數方法的簡寫方式來創建生成器,只需在函數名前添加一個星號(*)
var o = { *createIterator(items) { for (let i = 0; i < items.length; i++) { yield items[i]; } } }; let iterator = o.createIterator([1, 2, 3]);
這些示例使用了不同於之前的語法,但它們的功能實際上是等價的。在簡寫版本中,由於不使用function關鍵字來定義createlterator()方法,因此儘管可以在星號和方法名之間留白,但還是將星號緊貼在方法名之前
【狀態機】
生成器的一個常用功能是生成狀態機
let state = function*(){ while(1){ yield 'A'; yield 'B'; yield 'C'; } } let status = state(); console.log(status.next().value);//'A' console.log(status.next().value);//'B' console.log(status.next().value);//'C' console.log(status.next().value);//'A' console.log(status.next().value);//'B'
可迭代對象
可迭代對象具有Symbol.iterator屬性,是一種與迭代器密切相關的對象。Symbol.iterator通過指定的函數可以返回一個作用於附屬對象的迭代器。在ES6中,所有的集合對象(數組、Set集合及Map集合)和字元串都是可迭代對象,這些對象中都有預設的迭代器。ES6中新加入的特性for-of迴圈需要用到可迭代對象的這些功能
[註意]由於生成器預設會為Symbol.iterator屬性賦值,因此所有通過生成器創建的迭代器都是可迭代對象
一開始,我們曾提到過迴圈內部索引跟蹤的相關問題,要解決這個問題,需要兩個工具:一個是迭代器,另一個是for-of迴圈。如此一來,便不需要再跟蹤整個集合的索引,只需關註集合中要處理的內容
for-of迴圈每執行一次都會調用可迭代對象的next()方法,並將迭代器返回的結果對象的value屬性存儲在一個變數中,迴圈將持續執行這一過程直到返回對象的done屬性的值為true。這裡有個示例
let values = [1, 2, 3]; for (let num of values) { //1 //2 //3 console.log(num); }
這段for-of迴圈的代碼通過調用values數組的Symbol.iterator方法來獲取迭代器,這一過程是在JS引擎背後完成的。隨後迭代器的next()方法被多次調用,從其返回對象的value屬性讀取值並存儲在變數num中,依次為1、2和3,當結果對象的done屬性值為true時迴圈退出,所以num不會被賦值為undefined
如果只需迭代數組或集合中的值,用for-of迴圈代替for迴圈是個不錯的選擇。相比傳統的for迴圈,for-of迴圈的控制條件更簡單,不需要追蹤複雜的條件,所以更少出錯
[註意]如果將for-of語句用於不可迭代對象、null或undefined將會導致程式拋出錯誤
【訪問預設迭代器】
可以通過Symbol.iterator來訪問對象預設的迭代器
let values = [1, 2, 3]; let iterator = values[Symbol.iterator](); console.log(iterator.next()); // "{ value: 1, done: false }" console.log(iterator.next()); // "{ value: 2, done: false }" console.log(iterator.next()); // "{ value: 3, done: false }" console.log(iterator.next()); // "{ value: undefined, done: true }"
在這段代碼中,通過Symbol.iterator獲取了數組values的預設迭代器,並用它遍曆數組中的元素。在JS引擎中執行for-of迴圈語句時也會有類似的處理過程
由於具有Symbol.iterator屬性的對象都有預設的迭代器,因此可以用它來檢測對象是否為可迭代對象
function isIterable(object) { return typeof object[Symbol.iterator] === "function"; } console.log(isIterable([1, 2, 3])); // true console.log(isIterable("Hello")); // true console.log(isIterable(new Map())); // true console.log(isIterable(new Set())); // true console.log(isIterable(new WeakMap())); // false console.log(isIterable(new WeakSet())); // false
這裡的islterable()函數可以檢查指定對象中是否存在預設的函數類型迭代器,而for-of迴圈在執行前也會做相似的檢查
除了使用內建的可迭代對象類型的Symbol.iterator,也可以使用Symbol.iterator來創建屬於自己的迭代器
【創建可迭代對象】
預設情況下,開發者定義的對象都是不可迭代對象,但如果給Symbol.iterator屬性添加一個生成器,則可以將其變為可迭代對象
let collection = { items: [], *[Symbol.iterator]() { for (let item of this.items) { yield item; } } }; collection.items.push(1); collection.items.push(2); collection.items.push(3); for (let x of collection) { //1 //2 //3 console.log(x); }
在這個示例中,先創建一個生成器(註意,星號仍然在屬性名前)並將其賦值給對象的Symbol.iterator屬性來創建預設的迭代器;而在生成器中,通過for-of迴圈迭代this.items並用yield返回每一個值。collection對象預設迭代器的返回值由迭代器this.items自動生成,而非手動遍歷來定義返回值
【展開運算符和非數組可迭代對象】
通過展開運算符(...)可以把Set集合轉換成一個數組
let set = new Set([1, 2, 3, 3, 3, 4, 5]),
array = [...set];
console.log(array); // [1,2,3,4,5]
這段代碼中的展開運算符把Set集合的所有值填充到了一個數組字面量里,它可以操作所有可迭代對象,並根據預設迭代器來選取要引用的值,從迭代器讀取所有值。然後按照返回順序將它們依次插入到數組中。Set集合是一個可迭代對象,展開運算符也可以用於其他可迭代對象
let map = new Map([ ["name", "huochai"], ["age", 25]]),
array = [...map];
console.log(array); // [ ["name", "huochai"], ["age", 25]]
展開運算符把Map集合轉換成包含多個數組的數組,Map集合的預設迭代器返回的是多組鍵值對,所以結果數組與執行new Map()時傳入的數組看起來一樣
在數組字面量中可以多次使用展開運算符,將可迭代對象中的多個元素依次插入新數組中,替換原先展開運算符所在的位置
let smallNumbers = [1, 2, 3],
bigNumbers = [100, 101, 102],
allNumbers = [0, ...smallNumbers, ...bigNumbers];
console.log(allNumbers.length); // 7
console.log(allNumbers); // [0, 1, 2, 3, 100, 101, 102]
創建一個變數allNumbers,用展開運算符將smallNumbers和bigNumbers里的值依次添加到allNumbers中。首先存入0,然後存入small中的值,最後存入bigNumbers中的值。當然,原始數組中的值只是被覆制到allNumbers中,它們本身並未改變
由於展開運算符可以作用於任意可迭代對象,因此如果想將可迭代對象轉換為數組,這是最簡單的方法。既可以將字元串中的每一個字元(不是編碼單元)存入新數組中,也可以將瀏覽器中NodeList對象中的每一個節點存入新的數組中
內建迭代器
迭代器是ES6的一個重要組成部分,在ES6中,已經預設為許多內建類型提供了內建迭代器,只有當這些內建迭代器無法實現目標時才需要自己創建。通常來說當定義自己的對象和類時才會遇到這種情況,否則,完全可以依靠內建的迭代器完成工作,而最常使用的可能是集合的那些迭代器
【集合對象迭代器】
在ES6中有3種類型的集合對象:數組、Map集合與Set集合
為了更好地訪問對象中的內容,這3種對象都內建了以下三種迭代器
entries() 返回一個迭代器,其值為多個鍵值對
values() 返回一個迭代器,其值為集合的值
keys() 返回一個迭代器,其值為集合中的所有鍵名
調用以上3個方法都可以訪問集合的迭代器
entries()迭代器
每次調用next()方法時,entries()迭代器都會返回一個數組,數組中的兩個元素分別表示集合中每個元素的鍵與值。如果被遍歷的對象是數組,則第一個元素是數字類型的索引;如果是Set集合,則第一個元素與第二個元素都是值(Set集合中的值被同時作為鍵與值使用);如果是Map集合,則第一個元素為鍵名
let colors = [ "red", "green", "blue" ]; let tracking = new Set([1234, 5678, 9012]); let data = new Map(); data.set("title", "Understanding ES6"); data.set("format", "ebook"); for (let entry of colors.entries()) { console.log(entry); } for (let entry of tracking.entries()) { console.log(entry); } for (let entry of data.entries()) { console.log(entry); }
調用console.log()方法後輸出以下內容
[0, "red"] [1, "green"] [2, "blue"] [1234, 1234] [5678, 5678] [9012, 9012] ["title", "Understanding ES6"] ["format", "ebook"]
在這段代碼中,調用每個集合的entries()方法獲取一個迭代器,並使用for-of迴圈來遍歷元素,且通過console將每一個對象的鍵值對輸出出來
values()迭代器
調用values()迭代器時會返回集合中所存的所有值
let colors = [ "red", "green", "blue" ]; let tracking = new Set([1234, 5678, 9012]); let data = new Map(); data.set("title", "Understanding ES6"); data.set("format", "ebook"); for (let value of colors.values()) { console.log(value); } for (let value of tracking.values()) { console.log(value); } for (let value of data.values()) { console.log(value); }
調用console.log()方法後輸出以下內容
"red" "green" "blue" 1234 5678 9012 "Understanding ES6" "ebook"
如上所示,調用values()迭代器後,返回的是每個集合中包含的真正數據,而不包含數據在集合中的位置信息
keys()迭代器
keys()迭代器會返回集合中存在的每一個鍵。如果遍歷的是數組,則會返回數字類型的鍵,數組本身的其他屬性不會被返回;如果是Set集合,由於鍵與值是相同的,因此keys()和values()返回的也是相同的迭代器;如果是Map集合,則keys()迭代器會返回每個獨立的鍵
let colors = [ "red", "green", "blue" ]; let tracking = new Set([1234, 5678, 9012]); let data = new Map(); data.set("title", "Understanding ES6"); data.set("format", "ebook"); for (let key of colors.keys()) { console.log(key); } for (let key of tracking.keys()) { console.log(key); } for (let key of data.keys()) { console.log(key); }
調用console.log()方法後輸出以下內容
0 1 2 1234 5678 9012 "title" "format"
keys()迭代器會獲取colors、tracking和data這3個集合中的每一個鍵,而且分別在3個for-of迴圈內部將這些鍵名列印出來。對於數組對象來說,無論是否為數組添加命名屬性,列印出來的都是數字類型的索引;而for-in迴圈迭代的是數組屬性而不是數字類型的索引
不同集合類型的預設迭代器
每個集合類型都有一個預設的迭代器,在for-of迴圈中,如果沒有顯式指定則使用預設的迭代器。數組和Set集合的預設迭代器是values()方法,Map集合的預設迭代器是entries()方法。有了這些預設的迭代器,可以更輕鬆地在for-of迴圈中使用集合對象
let colors = [ "red", "green", "blue" ]; let tracking = new Set([1234, 5678, 9012]); let data = new Map(); data.set("title", "Understanding ES6"); data.set("format", "print"); // 與使用 colors.values() 相同 for (let value of colors) { console.log(value); } // 與使用 tracking.values() 相同 for (let num of tracking) { console.log(num); } // 與使用 data.entries() 相同 for (let entry of data) { console.log(entry); }
上述代碼未指定迭代器,所以將使用預設的迭代器。數組、Set集合及Map集合的預設迭代器也會反應出這些對象的初始化過程,所以這段代碼會輸出以下內容
"red" "green" "blue" 1234 5678 9012 ["title", "Understanding ES6"] ["format", "print"]
預設情況下,如果是數組和Set集合,會逐一返回集合中所有的值。如果是Map集合,則按照Map構造函數參數的格式返回相同的數組內容。而WeakSet集合與WeakMap集合就沒有內建的迭代器,由於要管理弱引用,因而無法確切地知道集合中存在的值,也就無法迭代這些集合了
【字元串迭代器】
自ES5發佈以後,JS字元串慢慢變得更像數組了,例如,ES5正式規定可以通過方括弧訪問字元串中的字元(也就是說,text[0]可以獲取字元串text的第一個字元,並以此類推)。由於方括弧操作的是編碼單元而非字元,因此無法正確訪問雙位元組字元
var message = "A