今天討論的新特性讓我非常興奮,因為這個特性是 ES6 中最神奇的特性。
這裡的「神奇」代表什麼呢?對於初學者來說,該特性與以往的 JS 完全不同,甚至有些晦澀難懂。從某種意義上說,它完全改變了這門語言的常見行為,這不是「神奇」是什麼。
不僅如此,此特性還可以簡化程式碼,將複雜的「回呼堆疊」改成直線執行的形式。
我是不是鋪墊的太多了?下面開始深入介紹,你自己去判斷吧。
簡介
什麼是 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(當下一個非常流行的應用程式)的一部分,點擊這裡試試,如果你對程式碼感到困惑,那就回到這裡來看下面的解釋。
這看起來很像一個函數,這被稱為 Generator 函數,它與我們常見的函數有很多共同點,但還可以看到下面兩個差異:
通常的函數以 function 開始,但 Generator 函數以 function* 開始。
在 Generator 函數內部,yield 是一個關鍵字,和 return 有點像。不同點在於,所有函數(包括 Generator 函數)都只能回傳一次,而在 Generator 函數中可以 yield 任意次。 yield 表達式暫停了 Generator 函數的執行,然後可以從暫停的地方恢復執行。
常見的函數不能暫停執行,而 Generator 函數可以,這就是這兩者最大的差異。
原理
呼叫 quips() 時發生了什麼事?
> 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 }
我們對普通函數的行為非常熟悉,函數被呼叫時就立即執行,直到函數返回或拋出一個異常,這是所有 JS 程式設計師的第二天性。
Generator 函數的呼叫方法與普通函數一樣:quips("jorendorff"),但呼叫一個Generator 函數時並沒有立即執行,而是傳回了一個Generator 物件(上面程式碼中的iter),這時函數就立即暫停在函數程式碼的第一行。
每次呼叫 Generator 物件的 .next() 方法時,函數就會開始執行,直到遇到下一個 yield 表達式為止。
這就是為什麼我們每次呼叫 iter.next() 時都會得到一個不同的字串,這些都是在函數內部透過 yield 表達式產生的值。
當執行最後一個 iter.next() 時,就到達了 Generator 函數的末尾,所以傳回結果的 .done屬性值為 true,並且 .value 屬性值為 undefined。
現在,回到 Talking cat 的 DEMO,嘗試在程式碼中加入一些 yield 表達式,看看會發生什麼。
從技術層面上講,每當Generator 函數執行遇到yield 表達式時,函數的棧幀— 本地變量,函數參數,臨時值和當前執行的位置,就從堆疊移除,但是Generator 物件保留了對該堆疊幀的引用,所以下次呼叫.next() 方法時,就可以恢復並繼續執行。
值得提醒的是 Generator 並不是多執行緒。在支援多執行緒的語言中,同一時間可以執行多段程式碼,並伴隨著執行資源的競爭,執行結果的不確定性和較好的效能。而 Generator 函數並不是這樣,當一個 Generator 函數執行時,它與其呼叫者都在同一線程中執行,每次執行順序都是確定的,有序的,並且執行順序不會改變。與執行緒不同,Generator 函數可以在內部的 yield 的標誌點暫停執行。
透過介紹 Generator 函數的暫停、執行和恢復執行,我們知道了什麼是 Generator 函數,那麼現在拋出一個問題:Generator 函數到底有什麼用呢?
迭代器
透過上篇文章,我們知道迭代器並不是ES6 的一個內建的類,而只是作為語言的一個擴展點,你可以透過實作[Symbol.iterator]() 和.next() 方法來定義一個迭代器。
但是,實作一個介面還是需要寫一些程式碼的,下面我們來看看在實際中如何實作一個迭代器,以實作一個range 迭代器為例,該迭代器只是簡單地從一個數累加到另一個數,有點像C 語言中的for (;;) 迴圈。
// 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); }
現在有一個解決方案,就是使用 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。
这种实现方式与 Java 和 Swift 的实现方式类似,看上去还不错,但还不能说上面代码就完全正确,代码没有任何 Bug?这很难说。我们看不到任何传统的 for (;;) 循环代码:迭代器的协议迫使我们将循环拆散了。
在这一点上,你也许会对迭代器不那么热衷了,它们使用起来很方便,但是实现起来似乎很难。
我们可以引入一种新的实现方式,以使得实现迭代器更加容易。上面介绍的 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 行的实现,替换掉整个 RangeIterator 类,这是因为 Generator 天生就是迭代器,所有的 Generator 都原生实现了 .next() 和 [Symbol.iterator]() 方法。你只需要实现其中的循环逻辑就够了。
不使用 Generator 去实现一个迭代器就像被迫写一个很长很长的邮件一样,本来简单的表达出你的意思就可以了,RangeIterator 的实现是冗长和令人费解的,因为它没有使用循环语法去实现一个循环功能。使用 Generator 才是我们需要掌握的实现方式。
我们可以使用作为迭代器的 Generator 的哪些功能呢?
使任何对象可遍历 — 编写一个 Genetator 函数去遍历 this,每遍历到一个值就 yield 一下,然后将该 Generator 函数作为要遍历的对象上的 [Symbol.iterator] 方法的实现。
简化返回数组的函数 — 假如有一个每次调用时都返回一个数组的函数,比如:
// 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 可以简化这类函数:
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); } }
这两者唯一的区别在于,前者在调用时计算出了所有结果并用一个数组返回,后者返回的是一个迭代器,结果是在需要的时候才进行计算,然后一个一个地返回。
无穷大的结果集 — 我们不能构建一个无穷大的数组,但是我们可以返回一个生成无尽序列的 Generator,并且每个调用者都可以从中获取到任意多个需要的值。
重构复杂的循环 — 你是否想将一个复杂冗长的函数重构为两个简单的函数?Generator 是你重构工具箱中一把新的瑞士军刀。对于一个复杂的循环,我们可以将生成数据集那部分代码重构为一个 Generator 函数,然后用 for-of 遍历:for (var data of myNewGenerator(args))。
构建迭代器的工具 — ES6 并没有提供一个可扩展的库,来对数据集进行 filter 和 map等操作,但 Generator 可以用几行代码就实现这类功能。
例如,假设你需要在 Nodelist 上实现与 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 很实用吧?当然,这是实现自定义迭代器最简单直接的方式,并且,在 ES6 中,迭代器是数据集和循环的新标准。
但,这还不是 Generator 的全部功能。
异步代码
异步 API 通常都需要一个回调函数,这意味着每次你都需要编写一个匿名函数来处理异步结果。如果同时处理三个异步事务,我们看到的是三个缩进层次的代码,而不仅仅是三行代码。
看下面代码:
}).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 有不同的约定。大多数情况下,错误是默认丢弃的,甚至有些将成功也默认丢弃了。
直到现在,这些问题仍是我们处理异步编程必须付出的代价,而且我们也已经接受了异步代码只是看不来不像同步代码那样简单和友好。
Generator 给我们带来了希望,我们可以不再采用上面的方式。
Q.async()是一个将 Generator 和 Promise 结合起来处理异步代码的实验性尝试,让我们的异步代码类似于相应的同步代码。
例如:
// 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(); }); }
最大的差別在於,需要在每個非同步方法呼叫的前面加上 yield 關鍵字。
在 Q.async 中,加入一個 if 語句或 try-catch 異常處理,就和在同步程式碼中的方式一樣,與其他編寫非同步程式碼的方式相比,減少了很多學習成本。
Generator 為我們提供了一個更適合人腦思維方式的非同步程式設計模型。但更好的語法也許更有幫助,在 ES7 中,一個基於 Promise 和 Generator 的非同步處理函數正在規劃中,靈感來自 C# 中類似的特性。
相容性
在伺服器端,現在就可以直接在 io.js 中使用 Generator(或在 NodeJs 中以 --harmony 啟動參數來啟動 Node)。
在瀏覽器端,目前只有 Firefox 27 和 Chrome 39 以上的版本才支援 Generator,如果想直接在 Web 上使用,你可以使用 Babel 或 Google 的 Traceur 將 ES6 程式碼轉換為 Web 友善的 ES5 程式碼。
一些題外話:JS 版本的 Generator 最早是由 Brendan Eich 實現,他借鑒了 Python Generator的實現,該實現的靈感來自 Icon,早在 2006 年的 Firefox 2.0 就吸納了 Generator。但標準化的道路是坎坷的,一路下來,其語法和行為都發生了很多改變,Firefox 和 Chrome 中的 ES6 Generator 是由 Andy Wingo 實現 ,這項工作是由 Bloomberg 贊助的。
yield;
關於 Generator 還有一些未提及的部分,我們還沒有涉及到 .throw() 和 .return() 方法的使用,.next() 方法的可選參數,還有 yield* 語法。但我認為這篇文章已經夠長了,就像 Generator 一樣,我們也暫停一下,另外找個時間再剩下的部分。
我們已經介紹了 ES6 中兩個非常重要的特性,那麼現在可以大膽地說,ES6 將改變我們的生活,看似簡單的特性,卻有極大的用處。