你不知道的JavaScript(中卷)|生成器(一)

打破完整運行
在第1 章中,我們解釋了JavaScript 開發(fā)者在代碼中幾乎普遍依賴的一個假定:一個函數(shù)一旦開始執(zhí)行,就會運行到結(jié)束,期間不會有其他代碼能夠打斷它并插入其間。
可能看起來似乎有點奇怪,不過ES6 引入了一個新的函數(shù)類型,它并不符合這種運行到結(jié)束的特性。這類新的函數(shù)被稱為生成器。

var x = 1;
function foo() {
    x++;
    bar(); // <-- 這一行是什么作用?
    console.log("x:", x);
}
function bar() {
    x++;
}
foo(); // x: 3

在這個例子中,我們確信bar() 會在x++ 和console.log(x) 之間運行。但是,如果bar()并不在那里會怎樣呢?顯然結(jié)果就會是2,而不是3。
現(xiàn)在動腦筋想一下。如果bar() 并不在那兒,但出于某種原因它仍然可以在x++ 和console.log(x) 語句之間運行,這又會怎樣呢?這如何才會成為可能呢?
如果是在搶占式多線程語言中,從本質(zhì)上說,這是可能發(fā)生的,bar() 可以在兩個語句之間打斷并運行。但JavaScript 并不是搶占式的,(目前)也不是多線程的。然而,如果foo() 自身可以通過某種形式在代碼的這個位置指示暫停的話,那就仍然可以以一種合作式的方式實現(xiàn)這樣的中斷(并發(fā))。
下面是實現(xiàn)這樣的合作式并發(fā)的ES6 代碼:

var x = 1;
function* foo() {
    x++;
    yield; // 暫停!
    console.log("x:", x);
}
function bar() {
    x++;
}

很可能你看到的其他多數(shù)JavaScript 文檔和代碼中的生成器聲明格式都是function* foo() { .. },而不是我這里使用的function foo() { .. }:唯一區(qū)別是 位置的風格不同。這兩種形式在功能和語法上都是等同的,還有一種是function*foo(){ .. }(沒有空格)也一樣。兩種風格,各有優(yōu)缺,但總體上我比較喜歡function foo.. 的形式,因為這樣在使用foo()來引用生成器的時候就會比較一致。如果只用foo() 的形式,你就不會清楚知道我指的是生成器還是常規(guī)函數(shù)。這完全是一個風格偏好問題。

現(xiàn)在,我們要如何運行前面的代碼片段,使得bar() 在*foo() 內(nèi)部的yield 處執(zhí)行呢?

// 構(gòu)造一個迭代器it來控制這個生成器
var it = foo();
// 這里啟動foo()!
it.next();
x; // 2
bar();
x; // 3
it.next(); // x: 3

(1) it = foo() 運算并沒有執(zhí)行生成器foo(),而只是構(gòu)造了一個迭代器(iterator),這個
迭代器會控制它的執(zhí)行。后面會介紹迭代器。
(2) 第一個it.next() 啟動了生成器
foo(),并運行了foo() 第一行的x++。
(3) foo() 在yield 語句處暫停,在這一點上第一個it.next() 調(diào)用結(jié)束。此時foo() 仍
在運行并且是活躍的,但處于暫停狀態(tài)。
(4) 我們查看x 的值,此時為2。
(5) 我們調(diào)用bar(),它通過x++ 再次遞增x。
(6) 我們再次查看x 的值,此時為3。
(7) 最后的it.next() 調(diào)用從暫停處恢復(fù)了生成器
foo() 的執(zhí)行,并運行console.log(..)
語句,這條語句使用當前x 的值3。

因此,生成器就是一類特殊的函數(shù),可以一次或多次啟動和停止,并不一定非得要完成。盡管現(xiàn)在還不是特別清楚它的強大之處,但隨著對本章后續(xù)內(nèi)容的深入學(xué)習(xí),我們會看到它將成為用于構(gòu)建以生成器作為異步流程控制的代碼模式的基礎(chǔ)構(gòu)件之一。

迭代消息傳遞

function* foo(x) {
    var y = x * (yield);
    return y;
}
var it = foo(6);
// 啟動foo(..)
it.next();
var res = it.next(7);
res.value; // 42

首先,傳入6 作為參數(shù)x。然后調(diào)用it.next(),這會啟動foo(..)。在foo(..) 內(nèi)部,開始執(zhí)行語句var y = x ..,但隨后就遇到了一個yield 表達式。它就會在這一點上暫停*foo(..)(在賦值語句中間!),并在本質(zhì)上要求調(diào)用代碼為yield表達式提供一個結(jié)果值。接下來,調(diào)用it.next( 7 ),這一句把值7 傳回作為被暫停的yield 表達式的結(jié)果。

兩個問題的故事
消息是雙向傳遞的——yield.. 作為一個表達式可以發(fā)出消息響應(yīng)next(..) 調(diào)用,next(..) 也可以向暫停的yield 表達式發(fā)送值。

function* foo(x) {
    var y = x * (yield "Hello"); // <-- yield一個值!
    return y;
}
var it = foo(6);
var res = it.next(); // 第一個next(),并不傳入任何東西
res.value; // "Hello"
res = it.next(7); // 向等待的yield傳入7
res.value; // 42

yield .. 和next(..) 這一對組合起來,在生成器的執(zhí)行過程中構(gòu)成了一個雙向消息傳遞系統(tǒng)。

我們并沒有向第一個next() 調(diào)用發(fā)送值,這是有意為之。只有暫停的yield才能接受這樣一個通過next(..) 傳遞的值,而在生成器的起始處我們調(diào)用第一個next() 時,還沒有暫停的yield 來接受這樣一個值。規(guī)范和所有兼容瀏覽器都會默默丟棄傳遞給第一個next() 的任何東西。傳值過去仍然不是一個好思路,因為你創(chuàng)建了沉默的無效代碼,這會讓人迷惑。因此,啟動生成器時一定要用不帶參數(shù)的next()。

第一個next() 調(diào)用(沒有參數(shù)的)基本上就是在提出一個問題:“生成器*foo(..) 要給我的下一個值是什么”。誰來回答這個問題呢?第一個yield "hello" 表達式。
但是,稍等!與yield 語句的數(shù)量相比,還是多出了一個額外的next()。所以,最后一個it.next(7) 調(diào)用再次提出了這樣的問題:生成器將要產(chǎn)生的下一個值是什么。但是,再沒有yield 語句來回答這個問題了,是不是?那么誰來回答呢?
return 語句回答這個問題!
如果你的生成器中沒有return 的話——在生成器中和在普通函數(shù)中一樣,return 當然不是必需的——總有一個假定的/ 隱式的return;(也就是 return undefined;),它會在默認情況下回答最后的it.next(7) 調(diào)用提出的問題。

多個迭代器
從語法使用的方面來看,通過一個迭代器控制生成器的時候,似乎是在控制聲明的生成器函數(shù)本身。但有一個細微之處很容易忽略:每次構(gòu)建一個迭代器,實際上就隱式構(gòu)建了生成器的一個實例,通過這個迭代器來控制的是這個生成器實例。
同一個生成器的多個實例可以同時運行,它們甚至可以彼此交互:

function* foo() {
    var x = yield 2;
    z++;
    var y = yield(x * z);
    console.log(x, y, z);
}
var z = 1;
var it1 = foo();
var it2 = foo();
var val1 = it1.next().value; // 2 <-- yield 2
var val2 = it2.next().value; // 2 <-- yield 2
val1 = it1.next(val2 * 10).value; // 40 <-- x:20, z:2
val2 = it2.next(val1 * 5).value; // 600 <-- x:200, z:3
it1.next(val2 / 2); // y:300
// 20 300 3
it2.next(val1 / 4); // y:10
// 200 10 3

同一個生成器的多個實例并發(fā)運行的最常用處并不是這樣的交互,而是生成器在沒有輸入的情況下,可能從某個獨立連接的資源產(chǎn)生自己的值。下一節(jié)中我們會詳細介紹值產(chǎn)生。

生成器產(chǎn)生值


生產(chǎn)者與迭代器
可以為我們的數(shù)字序列生成器實現(xiàn)標準的迭代器接口:

var something = (function() {
    var nextVal;
    return {
        // for..of循環(huán)需要
        [Symbol.iterator]: function() {
            return this;
        },
        // 標準迭代器接口方法
        next: function() {
            if (nextVal === undefined) {
                nextVal = 1;
            } else {
                nextVal = (3 * nextVal) + 6;
            }
            return {
                done: false,
                value: nextVal
            };
        }
    };
})();
something.next().value; // 1
something.next().value; // 9
something.next().value; // 33
something.next().value; // 105

next() 調(diào)用返回一個對象。這個對象有兩個屬性:done 是一個boolean 值,標識迭代器的完成狀態(tài);value 中放置迭代值。
ES6 還新增了一個for..of 循環(huán),這意味著可以通過原生循環(huán)語法自動迭代標準迭代器:

for (var v of something) {
    console.log(v);
    // 不要死循環(huán)!
    if (v > 500) {
        break;
    }
}
// 1 9 33 105 321 969

因為我們的迭代器something 總是返回done:false,因此這個for..of 循環(huán)將永遠運行下去,這也就是為什么我們要在里面放一個break 條件。迭代器永不結(jié)束是完全沒問題的,但是也有一些情況下,迭代器會在有限的值集合上運行,并最終返回done:true。

for..of 循環(huán)在每次迭代中自動調(diào)用next(),它不會向next() 傳入任何值,并且會在接收到done:true 之后自動停止。這對于在一組數(shù)據(jù)上循環(huán)很方便。

iterable
前面例子中的something 對象叫作迭代器,因為它的接口中有一個next() 方法。而與其緊密相關(guān)的一個術(shù)語是iterable(可迭代),即指一個包含可以在其值上迭代的迭代器的對象。
從ES6 開始,從一個iterable 中提取迭代器的方法是:iterable 必須支持一個函數(shù),其名稱是專門的ES6 符號值Symbol.iterator。調(diào)用這個函數(shù)時,它會返回一個迭代器。通常每次調(diào)用會返回一個全新的迭代器,雖然這一點并不是必須的。

var a = [1,3,5,7,9];
var it = a[Symbol.iterator]();
it.next().value; // 1
it.next().value; // 3
it.next().value; // 5
..

生成器迭代器
可以把生成器看作一個值的生產(chǎn)者,我們通過迭代器接口的next() 調(diào)用一次提取出一個值。
所以,嚴格說來,生成器本身并不是iterable,盡管非常類似——當你執(zhí)行一個生成器,就得到了一個迭代器。

停止生成器
在前面的例子中,看起來似乎*something() 生成器的迭代器實例在循環(huán)中的break 調(diào)用之后就永遠留在了掛起狀態(tài)。
其實有一個隱藏的特性會幫助你管理此事。for..of 循環(huán)的“異常結(jié)束”(也就是“提前終止”),通常由break、return 或者未捕獲異常引起,會向生成器的迭代器發(fā)送一個信號使其終止。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容