JavaScript 生成器的簡介

來源 | https://mp.weixin.qq.com/s/IdvvFfvgrwAt7DTJKnxGYA
一、單線程的 js
var a = 3;var b = 2;function foo() {a = a * 2;bar();b++;console.log(a, b); // 7, 5}function bar() {a++;b = b * 2;}
二、yield 暫停
var a = 3;var b = 2;function* foo() {// 聲明生成器a = a * 2;yield;b++;console.log(a, b);}function bar() {a++;b = b * 2;}
有些地方使用的是function *foo(){...} 的形式
那么現(xiàn)在要如何運行代碼才能達到想要的效果呢
/* 構(gòu)造一個迭代器it來控制生成器foo,此時foo完全沒有被執(zhí)行 */var it = foo();/* foo開始執(zhí)行,一直到遇到y(tǒng)ield才停止,此時a = 6, b = 2 */it.next();console.log(a, b); // 6, 2/* foo讓出線程,執(zhí)行bar,此時a = 7, b = 4 */bar();console.log(a, b); // 7, 4/* foo繼續(xù)執(zhí)行,此時a = 7, b = 5 */it.next(); // 7, 5
生成器是一類特殊的函數(shù),可以一次或多次啟動與停止,而且不一定非要完成
三、輸入與輸出
生成器特殊歸特殊,它始終都是一個函數(shù),依然具有一些函數(shù)的基本特性,例如接受參數(shù)(輸入),返回值(輸出)
function* foo(x, y) {return x * y;}var it = foo(6, 7);var res = it.next();console.log(res);// {value: 42, done: true}
next 的調(diào)用結(jié)果是一個對象,擁有一個 value 屬性,持有從 foo 返回的值,也就是說,yield 會導(dǎo)致生成器在執(zhí)行過程中發(fā)送出一個值,有點類似上面的return
3.1 迭代消息傳遞
除了能接收參數(shù)并提供返回值,生成器還提供了更強大的內(nèi)建消息輸入輸出能力,通過 yield 和 next 實現(xiàn)
function* foo(x) {var y = x * (yield);return y;}var it = foo(6);it.next();var res = it.next(7);console.log(res);// { value: 42, done: true }
首先 6 作為參數(shù) x,調(diào)用it.next() ,啟動foo
在foo 內(nèi)部,執(zhí)行語句var y = x... 時遇到y(tǒng)ield 表達式,在此處(賦值語句中間)暫停foo ,并要求調(diào)用代碼為yield 的表達式提供一個結(jié)果值,接下來調(diào)用it.next(7) ,這一句把 7 傳回作為被暫停的yield 表達式的結(jié)果
3.2 從兩個視角看next 與yield
一般來講,next 的數(shù)量要比yield 的數(shù)量多一個,這在理解代碼時會給人一種不協(xié)調(diào)不匹配的感覺
只考慮生成器代碼
var y = x * yield;return y;
第一個yield 提出一個問題:我這里應(yīng)該插入什么值?
誰來回答這個問題呢,第一個next 已經(jīng)運行使得生成器啟動并運行到此處,它顯然無法回答這個問題,因此必須由第二個next 調(diào)用回答第一個yield 提出的問題
這就是不匹配——第二個next對第一個yield?
接下來轉(zhuǎn)換一下視角,從迭代器角度看問題
在此之前需要再次解釋以下yield的消息傳遞,它是雙向的,前面我們只提了next向暫停的yield發(fā)送值,下面看一下yield發(fā)出消息供next使用。
function* foo(x) {var y = x * (yield "hello world");return y;}var it = foo(6);/* 第一個next不傳入任何東西,實際上就算傳了也會被瀏覽器悄咪咪丟掉 */var res = it.next();console.log(res); // { value: 'hello world', done: false }/* 給等待的yield傳一個7 */res = it.next(7);console.log(res); // { value: 42, done: true }
第一個next 調(diào)用提出一個問題:foo要給我的下一個值是什么?
誰來回答呢?第一個yield "hello world" 表達式
這里沒有不匹配的問題
但是,對于最后一個next ,也就是it.next(7) 提出的foo要給我的下一個值是什么的問題,沒有yield 來回答它了,那么由誰來回答呢
答案是return 語句
如果生成器中沒有return,則會有隱式的return undefined 來回答
四、多個迭代器
從語法使用的方面來看,通過一個迭代器控制生成器的時候,似乎是在控制聲明的生成器函數(shù)本身,但有一個細節(jié)需要注意,每次構(gòu)建一個迭代器,實際上就隱式構(gòu)建了生成器的一個實例,通過這個迭代器來控制其對應(yīng)的生成器實例
同一個生成器的多個實例可以同時運行,甚至彼此交互。
function* foo(i) {var x = yield 2;z++;var y = yield x * z;console.log({ i, x, y, z });}var z = 1;var it1 = foo(1);var it2 = foo(2);var val1 = it1.next().value;var val2 = it2.next().value;console.log({ val1, val2 }); // { val1: 2, val2: 2 }val1 = it1.next(val2 * 10).value; // z = 2, 1中x = 20, val1 = x * z = 40val2 = it2.next(val1 * 5).value; // z = 3, 2中x = 200, val2 = x * z = 600console.log({ val1, val2 }); // { val1: 40, val2: 600 }it1.next(val2 / 2); // { i: 1, x: 20, y: 300, z: 3 }it2.next(val1 / 4); // { i: 2, x: 200, y: 10, z: 3 }
再來看一個例子。
var a = 1;var b = 2;function foo() {a++;b = b * a;a = b + 3;}function bar() {b--;a = 8 + b;b = a * 2;}
上面是一串普通的 js 代碼,對于普通的 JS 函數(shù),顯然要么先執(zhí)行 foo,要么先執(zhí)行 bar,二者不能交替執(zhí)行
但是,使用生成器的話,交替執(zhí)行顯然是可能的
var a = 1;var b = 2;function* foo() {a++;yield;b = b * a;a = (yield b) + 3;}function* bar() {b--;yield;a = (yield 8) + b;b = a * (yield 2);}
根據(jù)每一步調(diào)用的相對順序的不同,上面的程序能產(chǎn)生多種不同的結(jié)果
首先構(gòu)建一個輔助函數(shù)來控制迭代器
function step(gen) {// 初始化一個生成器來創(chuàng)建迭代器itvar it = gen();var last;// 返回一個函數(shù),每次被調(diào)用都會將迭代器向前迭代一步,前面yield發(fā)出的值會在下一步發(fā)送回去return function () {// 不管yield出來的是什么下一次都把它原原本本的傳回去last = it.next(last).value;};}
下面我們先從最基本的情況開始,讓 foo 在 bar 之前執(zhí)行結(jié)束。
var s1 = step(foo);var s2 = step(bar);// 執(zhí)行foos1();s1();s1();// 執(zhí)行bars2();s2();s2();s2();console.log(a, b); // 11, 22
然后我們使二者交替進行
var s1 = step(foo);var s2 = step(bar);s2();// b--, b = 1, 遇到y(tǒng)ield,暫停!last接受第一個yield發(fā)出的值undefineds2();// 第一個yield返回值為undefined(無影響),執(zhí)行a = ...遇到y(tǒng)ield,暫停!last 接受第二個yield發(fā)出的值8s1();/* a++, a = 2, 遇到y(tǒng)ield,暫停!last接受第一個yield發(fā)出的值undefined */s2();// 第二個yield返回值為8,a = 8 + b = 8 + 1 = 9, 執(zhí)行b = 9 * ...遇到y(tǒng)ield,暫停!last 接受第三個yield發(fā)出的值2s1();/* 第一個yield返回值為undefined(無影響),b = b * a = 1 * 9 = 9, 執(zhí)行a = ..., 遇到y(tǒng)ield,暫停!last接受第二個yield發(fā)出的值b,也就是9 */s1();/* 第二個yield返回值為9,a = 9 + 3 = 12,s1執(zhí)行完畢,foo結(jié)束 */s2();// 第三個yield返回值為2,b = 9 * 2 = 18,s2執(zhí)行完畢,bar結(jié)束console.log(a, b); // 12, 18
五、迭代器與生成器
前面說了那么多“迭代器”與“生成器”,現(xiàn)在具體來談一談這二者是什么東東
5.1 迭代器
5.1.1 iterable
在 JavaScript 中,迭代器是一個對象,它定義一個序列,并在終止時可能返回一個返回值。更具體地說,迭代器是通過使用 next() 方法實現(xiàn) Iterator protocol 的任何一個對象,該方法返回具有兩個屬性的對象:value,這是序列中的 next值;和 done,如果已經(jīng)迭代到序列中的最后一個值,則它為 true 。如果 value 和 done 一起存在,則它是迭代器的返回值。
從 ES6 開始,從一個 iterable 中提取迭代器的方法是:iterable 必須支持以后函數(shù),其名稱是Symbol.iterable ,調(diào)用這個函數(shù)時,它會返回一個迭代器,通常每次調(diào)用會返回一個全新的迭代器
后面我們根據(jù)例子具體分析
5.1.2 生產(chǎn)者與迭代器
假定現(xiàn)在你要產(chǎn)生一系列值,每一個值都與前面一個有特定的關(guān)系,要實現(xiàn)這一點,需要一個有狀態(tài)的生產(chǎn)者能記住其生成的最后一個值
先用閉包整一個
var getSomething = (function () {var nextVal;return function () {if (nextVal === undefined) nextVal = 1;else nextVal = nextVal * 3 + 6;console.log(nextVal);return nextVal;};})();getSomething();getSomething();getSomething();getSomething();
再試著對它修改,將它改為一個標(biāo)準的迭代器接口。
var getSomething = (function () {var nextVal;return {[Symbol.iterator]: function () {return this;},next: function () {if (nextVal === undefined) nextVal = 1;else nextVal = nextVal * 3 + 6;return { done: nextVal > 500, value: nextVal };},};})();getSomething.next().value; // 1getSomething.next().value; // 9getSomething.next().value; // 33getSomething.next().value; // 105
其中有些令人疑惑的代碼大概就是[Symbol.iterable]: function() {return this} 這一行了
這一行的作用是將getSomething 的值也構(gòu)建成為一個 iterable,現(xiàn)在它既是 iterable 也是迭代器。
for (let v of getSomething) {console.log(v);}/*1933105321*/
5.2 生成器
可以把生成器看作一個值的生產(chǎn)者,我們通過迭代器接口的next 調(diào)用一次提取出一個值,所以嚴格來講,生成器本身并不是 iterrable,盡管執(zhí)行一個生成器就得到了一個迭代器
如果使用生成器來實現(xiàn)前面的getSometing :
function* getSomething() {var nextVal;while (true) {if (nextVal === undefined) nextVal = 1;else nextVal = nextVal * 3 + 6;yield nextVal;}}
生成器會在每個yield 處暫停,函數(shù)getSomething 的作用域會被保持,也就意味著不需要閉包在調(diào)用之間保持變量狀態(tài)
上面代碼也同樣可以使用for...of 循環(huán)
// 注意這里是getSomething(),得到了它的迭代器來進行循環(huán)的for (var v of getSomething()) {console.log(v);// 不要死循環(huán)!if (v > 500) {break;}}
但是上面的代碼看起來getSomething 在break執(zhí)行之后被永遠掛起了。
不過并非如此,for...of 循環(huán)的“異常結(jié)束”,通常由 break,return 或者未捕獲異常引起,會向生成器的迭代器發(fā)送一個信號使其終止。
學(xué)習(xí)更多技能
請點擊下方公眾號
![]()

