ES6語法筆記(Iterator、for...of、Generator、async)
**以下內容均摘自ECMAScript 6 入門——阮一峰
一、Iterator
遍歷器(Iterator)是一種介面,為各種不同的資料結構提供統一的訪問機制。任何資料結構只要部署 Iterator 介面,就可以完成遍歷操作。
Iterator 的作用有三個:一是為各種資料結構,提供一個統一的、簡便的訪問介面;二是使得資料結構的成員能夠按某種次序排列;三是 ES6 創造了一種新的遍歷命令for...of
迴圈,Iterator 介面主要供for...of
消費。
Iterator 的遍歷過程是這樣的。
(1)建立一個指標物件,指向當前資料結構的起始位置。也就是說,遍歷器物件本質上,就是一個指標物件。
(2)第一次呼叫指標物件的next
方法,可以將指標指向資料結構的第一個成員。
(3)第二次呼叫指標物件的next
方法,指標就指向資料結構的第二個成員。
(4)不斷呼叫指標物件的next
方法,直到它指向資料結構的結束位置。
每一次呼叫next
方法,都會返回資料結構的當前成員的資訊。具體來說,就是返回一個包含value
和done
兩個屬性的物件。其中,value
屬性是當前成員的值,done
屬性是一個布林值,表示遍歷是否結束。
ES6 規定,預設的 Iterator 介面部署在資料結構的Symbol.iterator
屬性,或者說,一個數據結構只要具有Symbol.iterator
屬性,就可以認為是“可遍歷的”(iterable)。
原生具備 Iterator 介面的資料結構如下。
Array Map Set String TypedArray 函式的 arguments 物件 NodeList 物件
//變數arr是一個數組,原生就具有遍歷器介面,部署在arr的Symbol.iterator屬性上面 let arr = ['a', 'b', 'c']; let iter = arr[Symbol.iterator](); iter.next() // { value: 'a', done: false } iter.next() // { value: 'b', done: false }iter.next() // { value: 'c', done: false } iter.next() // { value: undefined, done: true }
一個物件如果要具備可被for...of
迴圈呼叫的 Iterator 介面,就必須在Symbol.iterator
的屬性上部署遍歷器生成方法(原型鏈上的物件具有該方法也可)。
class RangeIterator { constructor(start, stop) { this.value = start; this.stop = stop; } [Symbol.iterator]() { return this; } next() { var value = this.value; if (value < this.stop) { this.value++; return {done: false, value: value}; } return {done: true, value: undefined}; } } function range(start, stop) { return new RangeIterator(start, stop); } for (var value of range(0, 3)) { console.log(value); // 0, 1, 2 }
字串是一個類似陣列的物件,也原生具有 Iterator 介面。
var someString = "hi"; typeof someString[Symbol.iterator] // "function" var iterator = someString[Symbol.iterator](); iterator.next() // { value: "h", done: false } iterator.next() // { value: "i", done: false } iterator.next() // { value: undefined, done: true }
Symbol.iterator
方法的最簡單實現,是使用Generator 函式。
let myIterable = { [Symbol.iterator]: function* () { yield 1; yield 2; yield 3; } } [...myIterable] // [1, 2, 3] // 或者採用下面的簡潔寫法 let obj = { * [Symbol.iterator]() { yield 'hello'; yield 'world'; } }; for (let x of obj) { console.log(x); } // "hello" // "world"
二、for...of
一個數據結構只要部署了Symbol.iterator
屬性,就被視為具有 iterator 介面,就可以用for...of
迴圈遍歷它的成員。
for...of
迴圈可以使用的範圍包括陣列、Set 和 Map 結構、某些類似陣列的物件(比如arguments
物件、DOM NodeList 物件)、Generator 物件,以及字串。
JavaScript 原有的for...in
迴圈,只能獲得物件的鍵名,不能直接獲取鍵值。ES6 提供for...of
迴圈,允許遍歷獲得鍵值。
var arr = ['a', 'b', 'c', 'd']; for (let a in arr) { console.log(a); // 0 1 2 3 } for (let a of arr) { console.log(a); // a b c d }
var engines = new Set(["Gecko", "Trident", "Webkit", "Webkit"]); for (var e of engines) { console.log(e); } // Gecko // Trident // Webkit var es6 = new Map(); es6.set("edition", 6); es6.set("committee", "TC39"); es6.set("standard", "ECMA-262"); for (var [name, value] of es6) { console.log(name + ": " + value); } // edition: 6 // committee: TC39 // standard: ECMA-262
並不是所有類似陣列的物件都具有 Iterator 介面,一個簡便的解決方法,就是使用Array.from
方法將其轉為陣列。
let arrayLike = { length: 2, 0: 'a', 1: 'b' }; // 報錯 for (let x of arrayLike) { console.log(x); } // 正確 for (let x of Array.from(arrayLike)) { console.log(x); }
對於普通的物件,for...of
結構不能直接使用,會報錯,必須部署了 Iterator 介面後才能使用。但是,這樣情況下,for...in
迴圈依然可以用來遍歷鍵名。
let es6 = { edition: 6, committee: "TC39", standard: "ECMA-262" }; for (let e in es6) { console.log(e); } // edition // committee // standard for (let e of es6) { console.log(e); } // TypeError: es6[Symbol.iterator] is not a function
可以使用Object.keys
方法將物件的鍵名生成一個數組,然後遍歷這個陣列。也可以使用 Generator 函式將物件重新包裝一下。
for (var key of Object.keys(someObject)) { console.log(key + ': ' + someObject[key]); } function* entries(obj) { for (let key of Object.keys(obj)) { yield [key, obj[key]]; } } for (let [key, value] of entries(obj)) { console.log(key, '->', value); } // a -> 1 // b -> 2 // c -> 3
三、Generator
Generator 函式是 ES6 提供的一種非同步程式設計解決方案。語法上,首先可以把它理解成,Generator 函式是一個狀態機,封裝了多個內部狀態。
執行 Generator 函式會返回一個遍歷器物件,也就是說,Generator 函式除了狀態機,還是一個遍歷器物件生成函式。返回的遍歷器物件,可以依次遍歷 Generator 函式內部的每一個狀態。
Generator 函式是一個普通函式,但是有兩個特徵。一是,function
關鍵字與函式名之間有一個星號;二是,函式體內部使用yield
表示式,定義不同的內部狀態(yield
在英語裡的意思就是“產出”)。
function* helloWorldGenerator() { yield 'hello'; yield 'world'; return 'ending'; } var hw = helloWorldGenerator(); hw.next() // { value: 'hello', done: false } hw.next() // { value: 'world', done: false } hw.next() // { value: 'ending', done: true } hw.next() // { value: undefined, done: true }
呼叫 Generator 函式,返回一個遍歷器物件,代表 Generator 函式的內部指標。以後,每次呼叫遍歷器物件的next
方法,就會返回一個有著value
和done
兩個屬性的物件。value
屬性表示當前的內部狀態的值,是yield
表示式後面那個表示式的值;done
屬性是一個布林值,表示是否遍歷結束。
由於 Generator 函式就是遍歷器生成函式,因此可以把 Generator 賦值給物件的Symbol.iterator
屬性,從而使得該物件具有 Iterator 介面
var myIterable = {}; myIterable[Symbol.iterator] = function* () { yield 1; yield 2; yield 3; }; [...myIterable] // [1, 2, 3]
Generator 函式從暫停狀態到恢復執行,它的上下文狀態(context)是不變的。通過next
方法的引數,就有辦法在 Generator 函式開始執行之後,繼續向函式體內部注入值。也就是說,可以在 Generator 函式執行的不同階段,從外部向內部注入不同的值,從而調整函式行為。
function* foo(x) { var y = 2 * (yield (x + 1)); var z = yield (y / 3); return (x + y + z); } var a = foo(5); a.next() // Object{value:6, done:false} a.next() // Object{value:NaN, done:false} a.next() // Object{value:NaN, done:true} var b = foo(5); b.next() // { value:6, done:false } b.next(12) // { value:8, done:false } b.next(13) // { value:42, done:true }
上面程式碼第一次呼叫b
的next
方法時,返回x+1
的值6
;第二次呼叫next
方法,將上一次yield
表示式的值設為12
,因此y
等於24
,返回y / 3
的值8
;第三次呼叫next
方法,將上一次yield
表示式的值設為13
,因此z
等於13
,這時x
等於5
,y
等於24
,所以return
語句的值等於42
。
從語義上講,第一個next
方法用來啟動遍歷器物件,所以不用帶有引數。
for...of
迴圈可以自動遍歷 Generator 函式時生成的Iterator
物件,且此時不再需要呼叫next
方法。
function* foo() { yield 1; yield 2; yield 3; yield 4; yield 5; return 6; } for (let v of foo()) { console.log(v); } // 1 2 3 4 5 //一旦next方法的返回物件的done屬性為true,for...of迴圈就會中止,且不包含該返回物件
Generator.prototype.throw()Generator 函式返回的遍歷器物件,都有一個throw
方法,可以在函式體外丟擲錯誤,然後在 Generator 函式體內捕獲。
var g = function* () { try { yield; } catch (e) { console.log('內部捕獲', e); } }; var i = g(); i.next(); try { i.throw('a'); i.throw('b'); } catch (e) { console.log('外部捕獲', e); } // 內部捕獲 a // 外部捕獲 b
歷器物件i
連續丟擲兩個錯誤。第一個錯誤被 Generator 函式體內的catch
語句捕獲。i
第二次丟擲錯誤,由於 Generator 函式內部的catch
語句已經執行過了,不會再捕捉到這個錯誤了,所以這個錯誤就被丟擲了 Generator 函式體,被函式體外的catch
語句捕獲。
throw
方法丟擲的錯誤要被內部捕獲,前提是必須至少執行過一次next
方法。
throw
方法被捕獲以後,會附帶執行下一條yield
表示式。也就是說,會附帶執行一次next
方法。
Generator.prototype.return()可以返回給定的值,並且終結遍歷 Generator 函式。
function* gen() { yield 1; yield 2; yield 3; } var g = gen(); g.next() // { value: 1, done: false } g.return('foo') // { value: "foo", done: true } g.next() // { value: undefined, done: true }
//如果 Generator 函式內部有try...finally程式碼塊,且正在執行try程式碼塊,那麼return方法會推遲到finally程式碼塊執行完再執行。 function* numbers () { yield 1; try { yield 2; yield 3; } finally { yield 4; yield 5; } yield 6; } var g = numbers(); g.next() // { value: 1, done: false } g.next() // { value: 2, done: false } g.return(7) // { value: 4, done: false } g.next() // { value: 5, done: false } g.next() // { value: 7, done: true }
next()
、throw()
、return()
這三個方法本質上是同一件事,可以放在一起理解。它們的作用都是讓 Generator 函式恢復執行,並且使用不同的語句替換yield
表示式。
如果yield
表示式後面跟的是一個遍歷器物件,需要在yield
表示式後面加上星號,表明它返回的是一個遍歷器物件。這被稱為yield*
表示式。
let delegatedIterator = (function* () { yield 'Hello!'; yield 'Bye!'; }()); let delegatingIterator = (function* () { yield 'Greetings!'; yield* delegatedIterator; yield 'Ok, bye.'; }()); for(let value of delegatingIterator) { console.log(value); } // "Greetings! // "Hello!" // "Bye!" // "Ok, bye."
//yield表示式返回整個字串,yield*語句返回單個字元。因為字串具有 Iterator 介面,所以被yield*遍歷。 let read = (function* () { yield 'hello'; yield* 'hello'; })(); read.next().value // "hello" read.next().value // "h"
物件的屬性是 Generator 函式,可以簡寫成下面的形式。
let obj = { * myGeneratorMethod() { ··· } }; //等同於 let obj = { myGeneratorMethod: function* () { // ··· } };
Generator 函式不是建構函式。返回的總是遍歷器物件,而不是this
物件。
function* g() { this.a = 11; } let obj = g(); obj.next(); obj.a // undefined function* F() { yield this.x = 2; yield this.y = 3; } new F() // TypeError: F is not a constructor //下面將其改造成建構函式 function* gen() { this.a = 1; yield this.b = 2; yield this.c = 3; } function F() { return gen.call(gen.prototype); } var f = new F(); f.next(); // Object {value: 2, done: false} f.next(); // Object {value: 3, done: false} f.next(); // Object {value: undefined, done: true} f.a // 1 f.b // 2 f.c // 3
Generator 函式是協程在 ES6 的實現,最大特點就是可以交出函式的執行權(即暫停執行)。
整個 Generator 函式就是一個封裝的非同步任務,或者說是非同步任務的容器。非同步操作需要暫停的地方,都用yield
語句註明。
Generator 函式可以暫停執行和恢復執行,這是它能封裝非同步任務的根本原因。除此之外,它還有兩個特性,使它可以作為非同步程式設計的完整解決方案:函式體內外的資料交換和錯誤處理機制。
next
返回值的 value 屬性,是 Generator 函式向外輸出資料;next
方法還可以接受引數,向 Generator 函式體內輸入資料。
Generator 函式內部還可以部署錯誤處理程式碼,捕獲函式體外丟擲的錯誤。
var fetch = require('node-fetch'); function* gen(){ var url = 'https://api.github.com/users/github'; var result = yield fetch(url); console.log(result.bio); } var g = gen(); var result = g.next(); result.value.then(function(data){ return data.json(); }).then(function(data){ g.next(data); }); //首先執行 Generator 函式,獲取遍歷器物件,然後使用next方法(第二行),執行非同步任務的第一階段。由於Fetch模組返回的是一個 Promise 物件,因此要用then方法呼叫下一個next方法。
Generator 非同步呼叫的自動執行器
基於 Thunk 函式的 Generator 自動執行器
基於co模組的Generator 自動執行器(實際上是基於Promise物件的自動執行)
四、async
async 函式是什麼?一句話,它就是 Generator 函式的語法糖。
async
函式就是將 Generator 函式的星號(*
)替換成async
,將yield
替換成await
,僅此而已。
async
函式的執行,與普通函式一模一樣,只要一行。
async
函式返回一個 Promise 物件,可以使用then
方法添加回調函式。
// 函式宣告 async function foo() {} // 函式表示式 const foo = async function () {}; // 物件的方法 let obj = { async foo() {} }; obj.foo().then(...) // Class 的方法 class Storage { constructor() { this.cachePromise = caches.open('avatars'); } async getAvatar(name) { const cache = await this.cachePromise; return cache.match(`/avatars/${name}.jpg`); } } const storage = new Storage(); storage.getAvatar('jake').then(…); // 箭頭函式 const foo = async () => {};
async
函式內部return
語句返回的值,會成為then
方法回撥函式的引數。
只有async
函式內部的非同步操作執行完,才會執行then
方法指定的回撥函式。
async function getTitle(url) { let response = await fetch(url); let html = await response.text(); return html.match(/<title>([\s\S]+)<\/title>/i)[1]; } getTitle('https://tc39.github.io/ecma262/').then(console.log) // "ECMAScript 2017 Language Specification" //函式getTitle內部有三個操作:抓取網頁、取出文字、匹配頁面標題。只有這三個操作全部完成,才會執行then方法裡面的console.log。
任何一個await
語句後面的 Promise 物件變為reject
狀態,那麼整個async
函式都會中斷執行。
我們希望即使前一個非同步操作失敗,也不要中斷後面的非同步操作。這時可以將第一個await
放在try...catch
結構裡面,這樣不管這個非同步操作是否成功,第二個await
都會執行。
多個await
命令後面的非同步操作,如果不存在繼發關係,最好讓它們同時觸發。
let foo = await getFoo(); let bar = await getBar(); //上面程式碼只有getFoo完成以後,才會執行getBar,完全可以讓它們同時觸發。 // 寫法一 let [foo, bar] = await Promise.all([getFoo(), getBar()]); // 寫法二 let fooPromise = getFoo(); let barPromise = getBar(); let foo = await fooPromise; let bar = await barPromise; //上面兩種寫法,getFoo和getBar都是同時觸發,這樣就會縮短程式的執行時間。
假定某個 DOM 元素上面,部署了一系列的動畫,前一個動畫結束,才能開始後一個。如果當中有一個動畫出錯,就不再往下執行,返回上一個成功執行的動畫的返回值。
async function chainAnimationsAsync(elem, animations) { let ret = null; try { for(let anim of animations) { ret = await anim(elem); } } catch(e) { /* 忽略錯誤,繼續執行 */ } return ret; }
按順序完成非同步操作,依次遠端讀取一組 URL,然後按照讀取的順序輸出結果。
//該方式為同步執行 //只有前一個 URL 返回結果,才會去讀取下一個 URL async function logInOrder(urls) { for (const url of urls) { const response = await fetch(url); console.log(await response.text()); } } //雖然map方法的引數是async函式,但它是併發執行的,因為只有async函式內部是繼發執行,外部不受影響。 async function logInOrder(urls) { // 併發讀取遠端URL const textPromises = urls.map(async url => { const response = await fetch(url); return response.text(); }); // 按次序輸出 for (const textPromise of textPromises) { console.log(await textPromise); } }
Iterator 介面是一種資料遍歷的協議,他的next
方法必須是同步的,只要呼叫就必須立刻返回值。
非同步遍歷器asyncIterator
的最大的語法特點,就是呼叫遍歷器的next
方法,返回的是一個 Promise 物件。物件的非同步遍歷器介面,部署在Symbol.asyncIterator
屬性上面。
非同步遍歷器的next
方法是可以連續呼叫的,不必等到上一步產生的 Promise 物件resolve
以後再呼叫。這種情況下,next
方法會累積起來,自動按照每一步的順序執行下去。
下面是非同步遍歷器的兩種使用方式
const asyncIterable = createAsyncIterable(['a', 'b']); const asyncIterator = asyncIterable[Symbol.asyncIterator](); asyncIterator .next() .then(iterResult1 => { console.log(iterResult1); // { value: 'a', done: false } return asyncIterator.next(); }) .then(iterResult2 => { console.log(iterResult2); // { value: 'b', done: false } return asyncIterator.next(); }) .then(iterResult3 => { console.log(iterResult3); // { value: undefined, done: true } }); async function f() { const asyncIterable = createAsyncIterable(['a', 'b']); const asyncIterator = asyncIterable[Symbol.asyncIterator](); console.log(await asyncIterator.next()); // { value: 'a', done: false } console.log(await asyncIterator.next()); // { value: 'b', done: false } console.log(await asyncIterator.next()); // { value: undefined, done: true } }
for...of
迴圈用於遍歷同步的 Iterator 介面。新引入的for await...of
迴圈,則是用於遍歷非同步的 Iterator 介面。
//createRejectingIterable()返回一個擁有非同步遍歷器介面的物件 //如果next方法返回的 Promise 物件被reject,for await...of就會報錯,要用try...catch捕捉。 async function () { try { for await (const x of createRejectingIterable()) { console.log(x); } } catch (e) { console.error(e); } }
就像 Generator 函式返回一個同步遍歷器物件一樣,非同步 Generator 函式的作用,是返回一個非同步遍歷器物件。
// 同步 Generator 函式 function* map(iterable, func) { const iter = iterable[Symbol.iterator](); while (true) { const {value, done} = iter.next(); if (done) break; yield func(value); } } // 非同步 Generator 函式 async function* map(iterable, func) { const iter = iterable[Symbol.asyncIterator](); while (true) { const {value, done} = await iter.next(); if (done) break; yield func(value); } }
普通的 async 函式返回的是一個 Promise 物件,而非同步 Generator 函式返回的是一個非同步 Iterator 物件。可以這樣理解,async 函式和非同步 Generator 函式,是封裝非同步操作的兩種方法,都用來達到同一種目的。區別在於,前者自帶執行器,後者通過for await...of
執行,或者自己編寫執行器
//一個非同步 Generator 函式的執行器 async function takeAsync(asyncIterable, count = Infinity) { const result = []; const iterator = asyncIterable[Symbol.asyncIterator](); while (result.length < count) { const {value, done} = await iterator.next(); if (done) break; result.push(value); } return result; } //使用例項 async function f() { async function* gen() { yield 'a'; yield 'b'; yield 'c'; } return await takeAsync(gen()); } f().then(function (result) { console.log(result); // ['a', 'b', 'c'] })
非同步 Generator 函數出現以後,JavaScript 就有了四種函式形式:普通函式、async 函式、Generator 函式和非同步 Generator 函式。請注意區分每種函式的不同之處。基本上,如果是一系列按照順序執行的非同步操作(比如讀取檔案,然後寫入新內容,再存入硬碟),可以使用 async 函式;如果是一系列產生相同資料結構的非同步操作(比如一行一行讀取檔案),可以使用非同步 Generator 函式。
yield*
語句也可以跟一個非同步遍歷器,for await...of
迴圈會展開yield*