Generator
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
方法可以恢復執行。
yield 表示式
由於 Generator 函式返回的遍歷器物件,只有呼叫next
方法才會遍歷下一個內部狀態,所以其實提供了一種可以暫停執行的函式。yield
表示式就是暫停標誌。因此等於為 JavaScript 提供了手動的“惰性求值”(Lazy Evaluation)的語法功能。
yield
表示式只能用在 Generator 函式裡面,用在其他地方都會報錯。
next 方法的引數
yield
表示式本身沒有返回值,或者說總是返回undefined
。next
方法可以帶一個引數,該引數就會被當作上一個yield
表示式的返回值。
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
。
如果向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
方法用來啟動遍歷器物件,所以不用帶有引數。
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 迴圈
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.prototype.throw()
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.prototype.return()
Generator 函式返回的遍歷器物件,還有一個return
方法,可以返回給定的值,並且終結遍歷 Generator 函式。
next()、throw()、return() 的共同點
本質上是同一件事,可以放在一起理解。它們的作用都是讓 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;
複製程式碼
yield* 表示式
如果在 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
語句的值。
實際上,任何資料結構只要有 Iterator 介面,就可以被yield*遍歷。
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 函式的this
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
複製程式碼