參考:ECMAScript 6 入門
Generator 函數(shù)概覽
調(diào)用 Generator 函數(shù)扬舒,返回一個遍歷器對象唆迁,代表 Generator 函數(shù)的內(nèi)部指針(類比Iterator)侥涵。以后卖陵,每次調(diào)用遍歷器對象的next方法,就會返回一個有著value和done兩個屬性的對象侧啼。value屬性表示當前的內(nèi)部狀態(tài)的值牛柒,是yield表達式后面那個表達式的值(yield本來就是產(chǎn)出的意思,它代表后面的計算值就是它的產(chǎn)出值)痊乾;done屬性是一個布爾值皮壁,表示是否遍歷結束。
舉例:
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 函數(shù)叫 Generator
因為 Generator 函數(shù)里用了yield啊哪审,yield就是產(chǎn)出的意思蛾魄,所以它叫生成器函數(shù)。
yield用法的注意事項(語法層面的東西我認為是非重點湿滓,如果開發(fā)過程中語法報錯滴须,查文檔就可以了):
- yield表達式如果用在另一個表達式之中,必須放在圓括號里面叽奥。
function* demo() {
console.log('Hello' + yield); // SyntaxError
console.log('Hello' + yield 123); // SyntaxError
console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK
}
- yield表達式用作函數(shù)參數(shù)或放在賦值表達式的右邊扔水,可以不加括號。
function* demo() {
foo(yield 'a', yield 'b'); // OK
let input = yield; // OK
}
Generator 與 Iterator 的關系:
由于 Generator 函數(shù)就是遍歷器生成函數(shù)朝氓,因此可以把 Generator 賦值給對象的Symbol.iterator屬性魔市,從而使得該對象具有 Iterator 接口主届。
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
我的問題:既然 Generator 實現(xiàn)的就是遍歷功能,只用 Iterator 就行了待德,為什么還多此一舉再弄一個 Generator?
理解yield的返回值:
next方法返回的是yield關鍵字后面表達式的計算值君丁,但是yield和它后面的表達式組合起來的表達式?jīng)]有返回值,或返回值是undefined磅网,如何理解谈截?
第一種情況舉例:
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 }
第二種情況舉例:
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}
我們預想的結果是:y為12,z為4涧偷,return值為(5+12+4)簸喂,但實際結果是:y為NaN,z為NaN燎潮,return值為(5+NaN+NaN)喻鳄,所以我們得出結論,在var y = 2 * (yield (x + 1));
這個語句中确封,(yield (x + 1))
這個表達式?jīng)]有返回值除呵。
那如何讓類似(yield (x + 1))
的表達式有返回值呢?
答案是通過next方法值爪喘,請看例子:
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
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(12)
中的12定義了(yield (x + 1))
的返回值。也就是說秉剑,next方法里傳的參數(shù)定義了上一個yield表達式的返回值(隱藏了一個信息就是泛豪,調(diào)用的第一個next方法不用傳參數(shù),即使傳了也沒用侦鹏,因為它沒有上一個yield表達式)诡曙。所以在上例中,x為5略水,y為2*12
价卤,z為13
,return值為42渊涝。
含有yield表達式的語句是如何運行的慎璧?
我本來以為第一次執(zhí)行next方法的結果是:Started, 1. undefined;同理驶赏,第二次執(zhí)行next方法是:2. undefined
但是炸卑,就我們看到的結果,很明顯執(zhí)行next方法的時候煤傍,它不會執(zhí)行它所在的語句盖文,只會返回yield后面表達式的計算值,另外從上一個包含yield的語句開始執(zhí)行蚯姆。
for...of 循環(huán):
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
兩點:
- 為什么打印結果沒有6?
一旦next方法的返回對象的done屬性為true五续,for...of循環(huán)就會中止洒敏,且不包含該返回對象,所以上面代碼的return語句返回的6疙驾,不包括在for...of循環(huán)之中凶伙。
- 為什么for循環(huán)里這樣寫
let v of foo()
?
因為我們要從遍歷器對象里遍歷,foo()
返回一個遍歷器對象它碎;
那為什么平時我們遍歷數(shù)組等時函荣,沒見調(diào)用方法?因為它默認隱式調(diào)用了扳肛。
無論解構還是什么傻挂,都是操作遍歷器對象的。比如:
let array = [1,2,3];
console.log(...array); // array默認調(diào)用了生成遍歷器對象的方法
function* numbers () {
yield 1
yield 2
return 3
yield 4
}
// 擴展運算符
[...numbers()] // [1, 2]挖息,必須要numbers()金拒,因為它才是遍歷器對象
Generator.prototype.throw() 知識點太瑣碎
內(nèi)部捕獲錯誤這種機制的好處:
這種函數(shù)體內(nèi)捕獲錯誤的機制,大大方便了對錯誤的處理套腹。多個yield表達式绪抛,可以只用一個try...catch代碼塊來捕獲錯誤。如果使用回調(diào)函數(shù)的寫法电禀,想要捕獲多個錯誤幢码,就不得不為每個函數(shù)內(nèi)部寫一個錯誤處理語句,現(xiàn)在只在 Generator 函數(shù)內(nèi)部寫一次catch語句就可以了尖飞。
我對上面這段話的理解是:
一般的做法蛤育,如果我們要對拋出的每個錯誤都處理,我們要這樣做:
try {
throw new Error('error 1');
} catch (e) {
console.log('error 1:', e);
}
try {
throw new Error('error 2');
} catch (e) {
console.log('error 2:', e);
}
如果寫成如下這樣我們?nèi)缦拢?/p>
try {
throw new Error('error 1');
throw new Error('error 2');
} catch (e) {
console.log(e);
}
當捕獲到error 1的錯誤后葫松,就不再執(zhí)行了。而且也沒有機會執(zhí)行throw new Error('error 2');
這句底洗。
使用Generator.prototype.throw()
就可以完美的將兩種優(yōu)點結合起來:
try {
g.throw('error 1'); // 這個錯誤將會在 Generator 函數(shù)內(nèi)部處理
g.throw('error 2'); // 這個錯誤將會交由下面的catch處理
} catch (e) {
console.log(e); // 打印 error 2
}
遍歷器對象拋出的錯誤優(yōu)先被 Generator 函數(shù)內(nèi)定義的catch捕獲處理腋么,如果還有多余的錯誤處理不了了,遍歷器對象將錯誤拋到外層亥揖。如果遍歷器對象拋出的錯誤內(nèi)外層都處理不了珊擂,那么程序將報錯,直接中斷執(zhí)行费变。
var g = function* () {
try {
yield;
} catch (e) {
console.log('內(nèi)部捕獲', e);
}
};
var i = g();
i.next();
try {
i.throw('a'); // 優(yōu)先被內(nèi)部的catch處理
i.throw('b'); // 內(nèi)部的catch已經(jīng)執(zhí)行過一次摧扇,已無法處理錯誤,拋出到外層挚歧,交由外層處理扛稽。
} catch (e) {
console.log('外部捕獲', e);
}
// 內(nèi)部捕獲 a
// 外部捕獲 b
throw方法拋出的錯誤要被內(nèi)部捕獲,前提是必須至少執(zhí)行過一次next方法滑负。
function* gen() {
try {
yield 1;
} catch (e) {
console.log('內(nèi)部捕獲');
}
}
var g = gen();
g.throw(1); //沒執(zhí)行過next方法在张,Generator 函數(shù)還沒有開始執(zhí)行用含,這時throw方法拋錯只可能拋出在函數(shù)外部。
// Uncaught 1
throw方法被捕獲以后帮匾,會附帶執(zhí)行下一條yield表達式啄骇。也就是說,會附帶執(zhí)行一次next方法瘟斜。
var gen = function* gen(){
try {
yield console.log('a');
} catch (e) {
// ...
}
yield console.log('b');
yield console.log('c');
}
var g = gen();
g.next() // a缸夹;因為yield的暫停功能,執(zhí)行完這個next方法后螺句,程序還停留在try代碼塊中虽惭,所以下面的throw才可以被catch
g.throw() // b; 附帶執(zhí)行一次next方法
g.next() // c
一旦 Generator 執(zhí)行過程中拋出錯誤,且沒有被內(nèi)部捕獲壹蔓,就不會再執(zhí)行下去了趟妥。如果此后還調(diào)用next方法,將返回一個value屬性等于undefined佣蓉、done屬性等于true的對象披摄,即 JavaScript 引擎認為這個 Generator 已經(jīng)運行結束了。(即使錯誤被外部捕獲勇凭,next方法也不會再執(zhí)行下去了)
function* g() {
yield 1;
console.log('throwing an exception');
throw new Error('generator broke!');
yield 2;
yield 3;
}
function log(generator) {
var v;
console.log('starting generator');
try {
v = generator.next();
console.log('第一次運行next方法', v);
} catch (err) {
console.log('捕捉錯誤', v);
}
try {
v = generator.next(); // 執(zhí)行到這里時疚膊,會執(zhí)行throw new Error('generator broke!')這句,拋出的這個錯誤又沒被內(nèi)部捕獲虾标,再執(zhí)行next方法寓盗,Generator 函數(shù)就已經(jīng)結束了
console.log('第二次運行next方法', v);
} catch (err) {
console.log('捕捉錯誤', v);
}
try {
v = generator.next();
console.log('第三次運行next方法', v);
} catch (err) {
console.log('捕捉錯誤', v);
}
console.log('caller done');
}
log(g());
// starting generator
// 第一次運行next方法 { value: 1, done: false }
// throwing an exception
// 捕捉錯誤 { value: 1, done: false }
// 第三次運行next方法 { value: undefined, done: true }
// caller done
Generator.prototype.return()
作用:
可以返回給定的值,并且終結遍歷 Generator 函數(shù)璧函。
舉例:
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next() // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true } 到這里就結束執(zhí)行了傀蚌,并返回了foo
g.next() // { value: undefined, done: true }
有一種情況,即使return了還會執(zhí)行蘸吓,那就是有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 }善炫,因為還有finally要執(zhí)行,所以運行不會結束库继,
// 它會當做執(zhí)行一次next方法箩艺,并設置了運行結束后的返回值
g.next() // { value: 5, done: false }
g.next()
// { value: 7, done: true },finally代碼塊執(zhí)行結束后宪萄,運行結束了艺谆,所以yield 6不會執(zhí)行。
// 因為設置了最終返回值是7拜英,所以執(zhí)行這個next返回7
讓我們來總結下都有哪些情況可以結束運行:
- 正常運行next到最后一個yield
- 遍歷器對象拋出的錯誤沒有被內(nèi)部捕獲
- 遍歷器對象調(diào)用return方法静汤,但finally會延遲結束情況的發(fā)生
next()、throw()、return() 的共同點
next()撒妈、throw()恢暖、return()這三個方法本質上是同一件事,它們的作用都是讓 Generator 函數(shù)恢復執(zhí)行狰右,并且使用不同的語句替換yield表達式杰捂。
- next(1)方法就相當于將yield表達式替換成一個值1。如果next方法沒有參數(shù)棋蚌,就相當于替換成undefined嫁佳。
- throw()是將yield表達式替換成一個throw語句。
- return()是將yield表達式替換成一個return語句谷暮。
如何在 Generator 函數(shù)中再調(diào)用 Generator 函數(shù)
關鍵詞:yield*
function* foo() {
yield 'a';
yield 'b';
}
function* bar() {
yield 'x';
yield* foo();
yield 'y';
}
// 等同于
function* bar() {
yield 'x';
yield 'a';
yield 'b';
yield 'y';
}
// 等同于
function* bar() {
yield 'x';
for (let v of foo()) {
yield v;
}
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// "x"
// "a"
// "b"
// "y"
帶 return 的 Generator 函數(shù)
單獨的:
Generator 函數(shù)中調(diào)用帶 return 的 Generator 函數(shù):
解釋:如果被代理的 Generator 函數(shù)有return語句蒿往,那么就可以向代理它的 Generator 函數(shù)返回數(shù)據(jù)。上面代碼在第四次調(diào)用next方法的時候湿弦,屏幕上會有輸出瓤漏,這是因為函數(shù)foo的return語句,向函數(shù)bar提供了返回值颊埃。
使用 Generator 函數(shù)進行遞歸
function* iterTree(tree) {
if (Array.isArray(tree)) {
for(let i=0; i < tree.length; i++) {
yield* iterTree(tree[i]); //在些遞歸
}
} else {
yield tree;
}
}
const tree = [ 'a', ['b', 'c'], ['d', 'e'] ];
for(let x of iterTree(tree)) {
console.log(x);
}
// a
// b
// c
// d
// e
[...iterTree(tree)] // ["a", "b", "c", "d", "e"]
Generator 函數(shù)作為對象屬性如何定義蔬充?
一般的:
let obj = {
myGeneratorMethod: function* () {
// ···
}
};
簡寫的:
let obj = {
* myGeneratorMethod() {
···
}
};
Generator 函數(shù)的this(要重點注意)
- Generator 函數(shù)總是返回一個遍歷器,ES6 規(guī)定這個遍歷器是 Generator 函數(shù)的實例班利,也繼承了 Generator 函數(shù)的prototype對象上的方法饥漫。而Generator 函數(shù)里面的this并不指的是遍歷器對象。
function* g() {
this.a = 11;
}
g.prototype.hello = function () {
return 'hi!';
};
let obj = g();
obj.next();
obj instanceof g // true
obj.hello() // 'hi!'
obj.a // undefined罗标;說明里面的this指向的不是遍歷器對象
- 如何使用 Generator 函數(shù)里面的this庸队?
答:Generator 函數(shù)本質上是加了語法糖的函數(shù),是函數(shù)就可以通過改變執(zhí)行上下文的方法改變里面的this指向(call或apply)
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var obj = {};
var f = F.call(obj); // 使里面的this指向obj
f.next(); // Object {value: 2, done: false}
f.next(); // Object {value: 3, done: false}
f.next(); // Object {value: undefined, done: true}
obj.a // 1
obj.b // 2
obj.c // 3
- 如果我想在遍歷器對象上訪問屬性呢闯割?基于上一個的方法彻消,再加上遍歷器對象可以訪問它的原型對象,那我們就把它的原型對象傳進去宙拉。
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var f = F.call(F.prototype);
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
- 如果我想使用new呢证膨?那就再上一步的基礎上再包一層
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