詳解JavaScript ES6中的Generator
今天討論的新特性讓我非常興奮,因為這個特性是 ES6 中最神奇的特性。
這里的“神奇”意味著什么呢?對于初學(xué)者來說,該特性與以往的 JS 完全不同,甚至有些晦澀難懂。從某種意義上說,它完全改變了這門語言的通常行為,這不是“神奇”是什么呢。
不僅如此,該特性還可以簡化程序代碼,將復(fù)雜的“回調(diào)堆?!备某芍本€執(zhí)行的形式。
我是不是鋪墊的太多了?下面開始深入介紹,你自己去判斷吧。
簡介
什么是 Generator?
看下面代碼:
function* quips(name) { yield "hello " + name + "!"; yield "i hope you are enjoying the blog posts"; if (name.startsWith("X")) { yield "it's cool how your name starts with X, " + name; } yield "see you later!"; } function* quips(name) { yield "hello " + name + "!"; yield "i hope you are enjoying the blog posts"; if (name.startsWith("X")) { yield "it's cool how your name starts with X, " + name; } yield "see you later!"; }
上面代碼是模仿Talking cat(當下一個非常流行的應(yīng)用)的一部分,點擊這里試玩,如果你對代碼感到困惑,那就回到這里來看下面的解釋。
這看上去很像一個函數(shù),這被稱為 Generator 函數(shù),它與我們常見的函數(shù)有很多共同點,但還可以看到下面兩個差異:
通常的函數(shù)以 function 開始,但 Generator 函數(shù)以 function* 開始。
在 Generator 函數(shù)內(nèi)部,yield 是一個關(guān)鍵字,和 return 有點像。不同點在于,所有函數(shù)(包括 Generator 函數(shù))都只能返回一次,而在 Generator 函數(shù)中可以 yield 任意次。yield 表達式暫停了 Generator 函數(shù)的執(zhí)行,然后可以從暫停的地方恢復(fù)執(zhí)行。
常見的函數(shù)不能暫停執(zhí)行,而 Generator 函數(shù)可以,這就是這兩者最大的區(qū)別。
原理
調(diào)用 quips() 時發(fā)生了什么?
> var iter = quips("jorendorff"); [object Generator] > iter.next() { value: "hello jorendorff!", done: false } > iter.next() { value: "i hope you are enjoying the blog posts", done: false } > iter.next() { value: "see you later!", done: false } > iter.next() { value: undefined, done: true } > var iter = quips("jorendorff"); [object Generator] > iter.next() { value: "hello jorendorff!", done: false } > iter.next() { value: "i hope you are enjoying the blog posts", done: false } > iter.next() { value: "see you later!", done: false } > iter.next() { value: undefined, done: true }
我們對普通函數(shù)的行為非常熟悉,函數(shù)被調(diào)用時就立即執(zhí)行,直到函數(shù)返回或拋出一個異常,這是所有 JS 程序員的第二天性。
Generator 函數(shù)的調(diào)用方法與普通函數(shù)一樣:quips("jorendorff"),但調(diào)用一個 Generator 函數(shù)時并沒有立即執(zhí)行,而是返回了一個 Generator 對象(上面代碼中的 iter),這時函數(shù)就立即暫停在函數(shù)代碼的第一行。
每次調(diào)用 Generator 對象的 .next() 方法時,函數(shù)就開始執(zhí)行,直到遇到下一個 yield 表達式為止。
這就是為什么我們每次調(diào)用 iter.next() 時都會得到一個不同的字符串,這些都是在函數(shù)內(nèi)部通過 yield 表達式產(chǎn)生的值。
當執(zhí)行最后一個 iter.next() 時,就到達了 Generator 函數(shù)的末尾,所以返回結(jié)果的 .done屬性值為 true,并且 .value 屬性值為 undefined。
現(xiàn)在,回到 Talking cat 的 DEMO,嘗試在代碼中添加一些 yield 表達式,看看會發(fā)生什么。
從技術(shù)層面上講,每當 Generator 函數(shù)執(zhí)行遇到 yield 表達式時,函數(shù)的棧幀 — 本地變量,函數(shù)參數(shù),臨時值和當前執(zhí)行的位置,就從堆棧移除,但是 Generator 對象保留了對該棧幀的引用,所以下次調(diào)用 .next() 方法時,就可以恢復(fù)并繼續(xù)執(zhí)行。
值得提醒的是 Generator 并不是多線程。在支持多線程的語言中,同一時間可以執(zhí)行多段代碼,并伴隨著執(zhí)行資源的競爭,執(zhí)行結(jié)果的不確定性和較好的性能。而 Generator 函數(shù)并不是這樣,當一個 Generator 函數(shù)執(zhí)行時,它與其調(diào)用者都在同一線程中執(zhí)行,每次執(zhí)行順序都是確定的,有序的,并且執(zhí)行順序不會發(fā)生改變。與線程不同,Generator 函數(shù)可以在內(nèi)部的 yield 的標志點暫停執(zhí)行。
通過介紹 Generator 函數(shù)的暫停、執(zhí)行和恢復(fù)執(zhí)行,我們知道了什么是 Generator 函數(shù),那么現(xiàn)在拋出一個問題:Generator 函數(shù)到底有什么用呢?
迭代器
通過上篇文章,我們知道迭代器并不是 ES6 的一個內(nèi)置的類,而只是作為語言的一個擴展點,你可以通過實現(xiàn) [Symbol.iterator]() 和 .next() 方法來定義一個迭代器。
但是,實現(xiàn)一個接口還是需要寫一些代碼的,下面我們來看看在實際中如何實現(xiàn)一個迭代器,以實現(xiàn)一個 range 迭代器為例,該迭代器只是簡單地從一個數(shù)累加到另一個數(shù),有點像 C 語言中的 for (;;) 循環(huán)。
// This should "ding" three times for (var value of range(0, 3)) { alert("Ding! at floor #" + value); } // This should "ding" three times for (var value of range(0, 3)) { alert("Ding! at floor #" + value); }
現(xiàn)在有一個解決方案,就是使用 ES6 的類。(如果你對 class 語法還不熟悉,不要緊,我會在將來的文章中介紹。)
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}; } else { return {done: true, value: undefined}; } } } // Return a new iterator that counts up from 'start' to 'stop'. function range(start, stop) { return new RangeIterator(start, stop); } 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}; } else { return {done: true, value: undefined}; } } } // Return a new iterator that counts up from 'start' to 'stop'. function range(start, stop) { return new RangeIterator(start, stop); }
查看該 DEMO。
這種實現(xiàn)方式與 Java 和 Swift 的實現(xiàn)方式類似,看上去還不錯,但還不能說上面代碼就完全正確,代碼沒有任何 Bug?這很難說。我們看不到任何傳統(tǒng)的 for (;;) 循環(huán)代碼:迭代器的協(xié)議迫使我們將循環(huán)拆散了。
在這一點上,你也許會對迭代器不那么熱衷了,它們使用起來很方便,但是實現(xiàn)起來似乎很難。
我們可以引入一種新的實現(xiàn)方式,以使得實現(xiàn)迭代器更加容易。上面介紹的 Generator 可以用在這里嗎?我們來試試:
function* range(start, stop) { for (var i = start; i < stop; i++) yield i; } function* range(start, stop) { for (var i = start; i < stop; i++) yield i; }
上面這 4 行代碼就可以完全替代之前的那個 23 行的實現(xiàn),替換掉整個 RangeIterator 類,這是因為 Generator 天生就是迭代器,所有的 Generator 都原生實現(xiàn)了 .next() 和 [Symbol.iterator]() 方法。你只需要實現(xiàn)其中的循環(huán)邏輯就夠了。
不使用 Generator 去實現(xiàn)一個迭代器就像被迫寫一個很長很長的郵件一樣,本來簡單的表達出你的意思就可以了,RangeIterator 的實現(xiàn)是冗長和令人費解的,因為它沒有使用循環(huán)語法去實現(xiàn)一個循環(huán)功能。使用 Generator 才是我們需要掌握的實現(xiàn)方式。
我們可以使用作為迭代器的 Generator 的哪些功能呢?
使任何對象可遍歷 — 編寫一個 Genetator 函數(shù)去遍歷 this,每遍歷到一個值就 yield 一下,然后將該 Generator 函數(shù)作為要遍歷的對象上的 [Symbol.iterator] 方法的實現(xiàn)。
簡化返回數(shù)組的函數(shù) — 假如有一個每次調(diào)用時都返回一個數(shù)組的函數(shù),比如:
// Divide the one-dimensional array 'icons' // into arrays of length 'rowLength'. function splitIntoRows(icons, rowLength) { var rows = []; for (var i = 0; i < icons.length; i += rowLength) { rows.push(icons.slice(i, i + rowLength)); } return rows; } // Divide the one-dimensional array 'icons' // into arrays of length 'rowLength'. function splitIntoRows(icons, rowLength) { var rows = []; for (var i = 0; i < icons.length; i += rowLength) { rows.push(icons.slice(i, i + rowLength)); } return rows; }
使用 Generator 可以簡化這類函數(shù):
function* splitIntoRows(icons, rowLength) { for (var i = 0; i < icons.length; i += rowLength) { yield icons.slice(i, i + rowLength); } } function* splitIntoRows(icons, rowLength) { for (var i = 0; i < icons.length; i += rowLength) { yield icons.slice(i, i + rowLength); } }
這兩者唯一的區(qū)別在于,前者在調(diào)用時計算出了所有結(jié)果并用一個數(shù)組返回,后者返回的是一個迭代器,結(jié)果是在需要的時候才進行計算,然后一個一個地返回。
無窮大的結(jié)果集 — 我們不能構(gòu)建一個無窮大的數(shù)組,但是我們可以返回一個生成無盡序列的 Generator,并且每個調(diào)用者都可以從中獲取到任意多個需要的值。
重構(gòu)復(fù)雜的循環(huán) — 你是否想將一個復(fù)雜冗長的函數(shù)重構(gòu)為兩個簡單的函數(shù)?Generator 是你重構(gòu)工具箱中一把新的瑞士軍刀。對于一個復(fù)雜的循環(huán),我們可以將生成數(shù)據(jù)集那部分代碼重構(gòu)為一個 Generator 函數(shù),然后用 for-of 遍歷:for (var data of myNewGenerator(args))。
構(gòu)建迭代器的工具 — ES6 并沒有提供一個可擴展的庫,來對數(shù)據(jù)集進行 filter 和 map等操作,但 Generator 可以用幾行代碼就實現(xiàn)這類功能。
例如,假設(shè)你需要在 Nodelist 上實現(xiàn)與 Array.prototype.filter 同樣的功能的方法。小菜一碟的事:
function* filter(test, iterable) { for (var item of iterable) { if (test(item)) yield item; } } function* filter(test, iterable) { for (var item of iterable) { if (test(item)) yield item; } }
所以,Generator 很實用吧?當然,這是實現(xiàn)自定義迭代器最簡單直接的方式,并且,在 ES6 中,迭代器是數(shù)據(jù)集和循環(huán)的新標準。
但,這還不是 Generator 的全部功能。
異步代碼
異步 API 通常都需要一個回調(diào)函數(shù),這意味著每次你都需要編寫一個匿名函數(shù)來處理異步結(jié)果。如果同時處理三個異步事務(wù),我們看到的是三個縮進層次的代碼,而不僅僅是三行代碼。
看下面代碼:
}).on('close', function () { done(undefined, undefined); }).on('error', function (error) { done(error); }); }).on('close', function () { done(undefined, undefined); }).on('error', function (error) { done(error); });
異步 API 通常都有錯誤處理的約定,不同的 API 有不同的約定。大多數(shù)情況下,錯誤是默認丟棄的,甚至有些將成功也默認丟棄了。
直到現(xiàn)在,這些問題仍是我們處理異步編程必須付出的代價,而且我們也已經(jīng)接受了異步代碼只是看不來不像同步代碼那樣簡單和友好。
Generator 給我們帶來了希望,我們可以不再采用上面的方式。
Q.async()是一個將 Generator 和 Promise 結(jié)合起來處理異步代碼的實驗性嘗試,讓我們的異步代碼類似于相應(yīng)的同步代碼。
例如:
// Synchronous code to make some noise. function makeNoise() { shake(); rattle(); roll(); } // Asynchronous code to make some noise. // Returns a Promise object that becomes resolved // when we're done making noise. function makeNoise_async() { return Q.async(function* () { yield shake_async(); yield rattle_async(); yield roll_async(); }); } // Synchronous code to make some noise. function makeNoise() { shake(); rattle(); roll(); } // Asynchronous code to make some noise. // Returns a Promise object that becomes resolved // when we're done making noise. function makeNoise_async() { return Q.async(function* () { yield shake_async(); yield rattle_async(); yield roll_async(); }); }
最大的區(qū)別在于,需要在每個異步方法調(diào)用的前面添加 yield 關(guān)鍵字。
在 Q.async 中,添加一個 if 語句或 try-catch 異常處理,就和在同步代碼中的方式一樣,與其他編寫異步代碼的方式相比,減少了很多學(xué)習(xí)成本。
Generator 為我們提供了一種更適合人腦思維方式的異步編程模型。但更好的語法也許更有幫助,在 ES7 中,一個基于 Promise 和 Generator 的異步處理函數(shù)正在規(guī)劃之中,靈感來自 C# 中類似的特性。
兼容性
在服務(wù)器端,現(xiàn)在就可以直接在 io.js 中使用 Generator(或者在 NodeJs 中以 --harmony 啟動參數(shù)來啟動 Node)。
在瀏覽器端,目前只有 Firefox 27 和 Chrome 39 以上的版本才支持 Generator,如果想直接在 Web 上使用,你可以使用 Babel 或 Google 的 Traceur 將 ES6 代碼轉(zhuǎn)換為 Web 友好的 ES5 代碼。
一些題外話:JS 版本的 Generator 最早是由 Brendan Eich 實現(xiàn),他借鑒了 Python Generator的實現(xiàn),該實現(xiàn)的靈感來自 Icon,早在 2006 年的 Firefox 2.0 就吸納了 Generator。但標準化的道路是坎坷的,一路下來,其語法和行為都發(fā)生了很多改變,F(xiàn)irefox 和 Chrome 中的 ES6 Generator 是由 Andy Wingo 實現(xiàn) ,這項工作是由 Bloomberg 贊助的。
yield;
關(guān)于 Generator 還有一些未提及的部分,我們還沒有涉及到 .throw() 和 .return() 方法的使用,.next() 方法的可選參數(shù),還有 yield* 語法。但我認為這篇文章已經(jīng)夠長了,就像 Generator 一樣,我們也暫停一下,另外找個時間再剩余的部分。
我們已經(jīng)介紹了 ES6 中兩個非常重要的特性,那么現(xiàn)在可以大膽地說,ES6 將改變我們的生活,看似簡單的特性,卻有極大的用處。
相關(guān)文章
JavaScript字符串對象substr方法入門實例(用于截取字符串)
這篇文章主要介紹了JavaScript字符串對象substr方法入門實例,substr用于根據(jù)開始位置和長度截取字符串,需要的朋友可以參考下2014-10-10Javascript學(xué)習(xí)筆記2 函數(shù)
在Javascript中,function才是Javascript的第一型。當我們寫下一段函數(shù)時,其實不過是建立了一個function類型的實體。2010-01-01JavaScript前端圖片加載管理器imagepool使用詳解
這篇文章主要介紹了JavaScript前端圖片加載管理器imagepool使用詳解,需要的朋友可以參考下2014-12-12JavaScript高級程序設(shè)計(第3版)學(xué)習(xí)筆記4 js運算符和操作符
如果說數(shù)據(jù)類型是編程語言的磚瓦,那么運算符和操作符則是編程語言的石灰和水泥了,它是將各種數(shù)據(jù)類型的值有機組合的糅合劑,使得數(shù)據(jù)值不再只是一個孤立的值,而有了一種動態(tài)的靈性2012-10-10