Generator 函數有多種理解角度。語法上,首先能夠把它理解成,Generator 函數是一個狀態機,封裝了多個內部狀態。數組
執行 Generator 函數會返回一個遍歷器對象,也就是說,Generator 函數除了狀態機,仍是一個遍歷器對象生成函數。返回的遍歷器對象,能夠依次遍歷 Generator 函數內部的每個狀態。數據結構
Generator 函數有兩個特徵:函數
function
關鍵字與函數名之間有一個星號
yield
表達式,定義不一樣的內部狀態(yield
在英語裏的意思就是「產出」)function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
hw.next()
// { value: 'hello', done: false } value屬性就是當前yield表達式的值,done屬性爲false,表示遍歷尚未結束。
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true } done屬性爲true,表示遍歷已經結束。
複製代碼
上面代碼定義了一個 Generator 函數helloWorldGenerator
,它內部有兩個yield
表達式(hello
和world
),即該函數有三個狀態:hello,world 和 return 語句(結束執行)。
調用方法與普通函數同樣,可是調用,函數並不執行,返回一個指向內部狀態的指針對象,也就是遍歷器對象。 必須調用遍歷器對象的next
方法,使得指針移向下一個狀態。內部指針就從函數頭部或上一次停下來的地方開始執行,直到遇到下一個yield
表達式(或return
語句)爲止。
換言之,Generator 函數是分段執行的,yield
表達式是暫停執行的標記,而next
方法能夠恢復執行。this
因爲 Generator 函數返回的遍歷器對象,只有調用next
方法纔會遍歷下一個內部狀態,因此其實提供了一種能夠暫停執行的函數。yield
表達式就是暫停標誌。所以等於爲 JavaScript 提供了手動的「惰性求值」(Lazy Evaluation)的語法功能。
yield
表達式只能用在 Generator 函數裏面,用在其餘地方都會報錯。lua
yield
表達式自己沒有返回值,或者說老是返回undefined
。next
方法能夠帶一個參數,該參數就會被看成上一個yield
表達式的返回值。spa
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}
var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }
複製代碼
上面代碼中,第二次運行next
方法的時候不帶參數,致使 y
的值等於2 * undefined
(即NaN
),除以 3
之後仍是NaN
,所以返回對象的value
屬性也等於NaN
。第三次運行Next
方法的時候不帶參數,因此z
等於undefined
,返回對象的value
屬性等於5 + NaN + undefined
,即NaN
。prototype
若是向next
方法提供參數,返回結果就徹底不同了。上面代碼第一次調用b的next
方法時,返回x+1
的值6
;第二次調用next
方法,將上一次yield
表達式的值設爲12
,所以y
等於24
,返回y / 3
的值8
;第三次調用next
方法,將上一次yield
表達式的值設爲13
,所以z等於13
,這時x
等於5
,y
等於24
,因此return
語句的值等於42
。指針
注意,因爲next
方法的參數表示上一個yield
表達式的返回值,因此在第一次使用next
方法時,傳遞參數是無效的。V8 引擎直接忽略第一次使用next
方法時的參數,只有從第二次使用next
方法開始,參數纔是有效的。從語義上講,第一個next
方法用來啓動遍歷器對象,因此不用帶有參數。code
next
方法的參數,也能夠向Generator 函數內部輸入值對象
function* dataConsumer() {
console.log('Started');
console.log(`1. ${yield}`);
console.log(`2. ${yield}`);
return 'result';
}
let genObj = dataConsumer();
genObj.next();
// Started
genObj.next('a')
// 1. a
genObj.next('b')
// 2. b
複製代碼
for...of
循環能夠自動遍歷 Generator 函數時生成的Iterator
對象,且此時再也不須要調用next
方法。
function* foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6; //return語句返回的,不包括在for...of循環之中
}
for (let v of foo()) {
console.log(v);
}
// 1 2 3 4 5
複製代碼
除了for...of
循環之外,擴展運算符(...
)、解構賦值和Array.from
方法內部調用的,都是遍歷器接口。這意味着,它們均可以將 Generator 函數返回的 Iterator 對象,做爲參數。
function* numbers () {
yield 1
yield 2
return 3
yield 4
}
// 擴展運算符
[...numbers()] // [1, 2]
// Array.from 方法
Array.from(numbers()) // [1, 2]
// 解構賦值
let [x, y] = numbers();
x // 1
y // 2
// for...of 循環
for (let n of numbers()) {
console.log(n)
}
// 1
// 2
複製代碼
Generator 函數返回的遍歷器對象,都有一個throw
方法,能夠在函數體外拋出錯誤,而後在 Generator 函數體內捕獲。
var g = function* () {
try {
yield;
} catch (e) {
console.log('內部捕獲', e);
}
};
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕獲', e);
}
// 內部捕獲 a
// 外部捕獲 b
複製代碼
一旦執行了catch
,捕捉了錯誤,Generator 函數就已經結束了,再也不執行下去了。
Generator 函數返回的遍歷器對象,還有一個return
方法,能夠返回給定的值,而且終結遍歷 Generator 函數。
本質上是同一件事,能夠放在一塊兒理解。它們的做用都是讓 Generator 函數恢復執行,而且使用不一樣的語句替換yield
表達式。
next()
是將yield
表達式替換成一個值。
const g = function* (x, y) {
let result = yield x + y;
return result;
};
const gen = g(1, 2);
gen.next(); // Object {value: 3, done: false}
gen.next(1); // Object {value: 1, done: true}
// 至關於將 let result = yield x + y
// 替換成 let result = 1;
複製代碼
throw()
是將yield
表達式替換成一個throw
語句。
gen.throw(new Error('出錯了')); // Uncaught Error: 出錯了
// 至關於將 let result = yield x + y
// 替換成 let result = throw(new Error('出錯了'));
複製代碼
return()
是將yield
表達式替換成一個return
語句。
gen.return(2); // Object {value: 2, done: true}
// 至關於將 let result = yield x + y
// 替換成 let result = return 2;
複製代碼
若是在 Generator 函數內部,調用另外一個 Generator 函數,默認狀況下是沒有效果的。yield*
表達式,用來在一個 Generator 函數裏面執行另外一個 Generator 函數。
function* foo() {
yield 'a';
yield 'b';
}
//普通方法調用foo() ==========================
function* bar() {
yield 'x';
foo();
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// "x"
// "y"
//上面foo()的調用是沒有效果的
//yield*表達式調用 =================================
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"
複製代碼
從語法角度看,若是yield
表達式後面跟的是一個遍歷器對象,須要在yield表達式後面加上星號,代表它返回的是一個遍歷器對象。這被稱爲yield*
表達式。
yield*
後面的 Generator 函數(沒有return語句時),等同於在 Generator 函數內部,部署一個for...of
循環。
function* concat(iter1, iter2) {
yield* iter1;
yield* iter2;
}
// 等同於
function* concat(iter1, iter2) {
for (var value of iter1) {
yield value;
}
for (var value of iter2) {
yield value;
}
}
複製代碼
上面代碼說明,yield*
後面的 Generator 函數(沒有return
語句時),不過是for...of
的一種簡寫形式,徹底能夠用後者替代前者。反之,在有return
語句時,則須要用var value = yield* iterator
的形式獲取return
語句的值。
yield*
命令能夠很方便地取出嵌套數組的全部成員。
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
複製代碼
Generator 函數g返回的遍歷器obj
,是g的實例,並且繼承了g.prototype
。可是,若是把g看成普通的構造函數,並不會生效,由於g返回的老是遍歷器對象,而不是this
對象,也不能跟new命令一塊兒用,會報錯。
下面是一個變通方法。首先,生成一個空對象,使用call
方法綁定 Generator 函數內部的this。這樣,構造函數調用之後,這個空對象就是 Generator 函數的實例對象了。
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
var obj = {};
var f = F.call(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
複製代碼
還有一個辦法就是將obj換成F.prototype
。
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
複製代碼
再將F改爲構造函數,就能夠對它執行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
複製代碼