ES6學習筆記14 Generator 函式
簡介
Generator
函式有多種理解角度。語法上,首先可以把它理解成,Generator
函式是一個狀態機,封裝了多個內部狀態。
執行 Generator
函式會返回一個遍歷器物件,也就是說,Generator
函式除了狀態機,還是一個遍歷器物件生成函式。返回的遍歷器物件,可以依次遍歷 Generator
函式內部的每一個狀態。
形式上,Generator
函式是一個普通函式,但是有兩個特徵。一是,function
關鍵字與函式名之間有一個星號;二是,函式體內部使用yield
表示式,定義不同的內部狀態。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
呼叫 Generator
函式後,該函式並不執行,返回的也不是函式執行結果,而是一個指向內部狀態的指標物件,也就是遍歷器物件
下一步,必須呼叫遍歷器物件的next
方法,使得指標移向下一個狀態。每次呼叫next
方法,內部指標就從函式頭部或上一次停下來的地方開始執行,直到遇到下一個yield
表示式(或return
語句)為止,如果沒有再遇到新的yield
表示式,就一直執行到函式結束。換言之,Generator
函式是分段執行的,yield
next
方法可以恢復執行。
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
yield表示式
yield
表示式後面的表示式,只有當呼叫next
方法、內部指標指向該語句時才會執行,因此等於為 JavaScript 提供了手動的“惰性求值”(Lazy Evaluation)的語法功能。
function* gen() {
yield 123 + 456; //後面的表示式只有當呼叫next方法時,才會計算求值
}
yield
表示式只能用在 Generator
函式裡面,用在其他地方都會報錯。另外,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
表示式用作函式引數或放在賦值表示式的右邊,可以不加括號
function* demo() {
foo(yield 'a', yield 'b'); // OK
let input = yield; // OK
}
與Iterator介面的關係
由於 Generator
函式就是遍歷器生成函式,因此可以把 Generator
賦值給物件的Symbol.iterator
屬性,從而使得該物件具有 Iterator
介面。
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
[...myIterable] // [1, 2, 3]
Generator
函式執行後,返回一個遍歷器物件。該物件本身也具有Symbol.iterator
屬性,執行後返回自身。
function* gen(){
// some code
}
var g = gen();
g[Symbol.iterator]() === g
// true
next方法的引數
yield
表示式本身沒有返回值,或者說總是返回undefined。next
方法可以帶一個引數,該引數就會被當作上一個yield
表示式的返回值。
function* f() {
for(var i = 0; true; i++) {
var reset = yield i; //reset為yield表示式的返回值
if(reset) { i = -1; } //當reset不為undefined時,i賦值為-1
}
}
var g = f();
g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }
這個功能有很重要的語法意義。Generator
函式從暫停狀態到恢復執行,它的上下文狀態(context)是不變的。通過next
方法的引數,就有辦法在 Generator
函式開始執行之後,繼續向函式體內部注入值。
注意,由於next
方法的引數表示上一個yield
表示式的返回值,所以在第一次使用next
方法時,傳遞引數是無效的。要想在第一次呼叫時就能輸入值,可以在 Generator
函式外面再包一層。
function wrapper(generatorFunction) {
return function (...args) {
let generatorObject = generatorFunction(...args); //呼叫原始的Generator函式
generatorObject.next(); //先執行一次next方法
return generatorObject; //返回遍歷器物件
};
}
const wrapped = wrapper(function* () {
console.log(`First input: ${yield}`);
return 'DONE';
});
wrapped().next('hello!')
// First input: hello!
上述程式碼在wrapper函式中,首先呼叫一次next
方法,再返回遍歷器物件。當用戶自己呼叫next
方法時,看起來就像是第一次呼叫,但實際上,這是第二次呼叫next
方法。
for…of 迴圈
for...of
迴圈可以自動遍歷 Generator 函式時生成的Iterator
物件,且此時不再需要呼叫next方法,但是遍歷不包含return
語句的返回值
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
除了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.prototype.return()
Generator
函式返回的遍歷器物件,都有一個throw
方法,可以在函式體外丟擲錯誤,然後在 Generator
函式體內捕獲。throw
方法可以接受一個引數,建議丟擲Error
物件的例項。
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
遍歷器物件的throw
方法與全域性的throw
命令相比,前者丟擲的錯誤可以被Generator
函式內部的catch
語句(優先捕獲)和函式外部的catch
語句捕獲,後者丟擲的錯誤只能被函式體外的catch
語句捕獲
var g = function* () {
while (true) {
yield;
console.log('內部捕獲', e);
}
};
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕獲', e);
}
// 外部捕獲 a
如果 Generator
函式內部和外部,都沒有部署try...catch
程式碼塊,那麼throw方法將導致程式報錯,直接中斷執行。
throw
方法被捕獲以後,會附帶執行下一條yield
表示式。也就是說,會附帶執行一次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
g.throw() // b
g.next() // c
Generator
函式體外丟擲的錯誤,可以在函式體內捕獲;反過來,Generator
函式體內丟擲的錯誤,也可以被函式體外的catch
捕獲。一旦 Generator
執行過程中丟擲錯誤,且沒有被內部捕獲,就不會再執行下去了。如果此後還呼叫next方法,將返回一個value屬性等於undefined
、done
屬性等於true的物件,即 JavaScript 引擎認為這個 Generator 已經執行結束了。
function* foo() {
var x = yield 3;
var y = x.toUpperCase(); //報錯
yield y;
}
var it = foo();
it.next(); // { value:3, done:false }
try {
it.next(42);
} catch (err) {
console.log(err); //捕獲
}
Generator
函式返回的遍歷器物件,還有一個return
方法,可以返回給定的值,並且終結遍歷 Generator
函式。如果不提供引數,則返回undefined
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next() // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next() // { value: undefined, done: true }
如果Generator
函式內部有try...finally
程式碼塊,那麼return
方法會推遲到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 }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }
比較遍歷器物件的next()
、throw()
和return()
方法,它們的作用都是讓Generator
函式恢復執行,並且使用不同的語句替換yield
表示式。next()
是將yield
表示式替換成一個值,throw()
是將yield
表示式替換成一個throw
語句,而return()
是將yield
表示式替換成一個return
語句。
yield* 表示式
如果在 Generator
函式內部,呼叫另一個 Generator
函式,預設情況下是沒有效果的。而yield*
表示式,用來在一個 Generator
函式裡面執行另一個 Generator
函式。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*
後面跟著資料結構,只要該資料結構具有Iterator介面,就可以被yield*
遍歷
function* gen(){
yield* ["a", "b", "c"];
}
gen().next() // { value:"a", done:false }
let read = (function* () {
yield 'hello';
yield* 'hello';
})();
read.next().value // "hello"
read.next().value // "h"
如果被代理的 Generator
函式有return
語句,那麼就可以向代理它的 Generator
函式返回資料。
function* genFuncWithReturn() {
yield 'a';
yield 'b';
return 'The result';
}
function* logReturned(genObj) {
let result = yield* genObj;
console.log(result);
}
[...logReturned(genFuncWithReturn())]
// The result
// 值為 [ 'a', 'b' ]
上面程式碼中,存在兩次遍歷。第一次是擴充套件運算子遍歷函式logReturned
返回的遍歷器物件,第二次是yield*
語句遍歷函式genFuncWithReturn
返回的遍歷器物件。genFuncWithReturn
的return
語句的返回值The result,會返回給函式logReturned
內部的result
變數,因此會有終端輸出
使用yield*
語句完全遍歷二叉樹
// 下面是二叉樹的建構函式,
// 三個引數分別是左樹、當前節點和右樹
function Tree(left, label, right) {
this.left = left;
this.label = label;
this.right = right;
}
// 下面是中序(inorder)遍歷函式。
// 由於返回的是一個遍歷器,所以要用generator函式。
// 函式體內採用遞迴演算法,所以左樹和右樹要用yield*遍歷
function* inorder(t) {
if (t) {
yield* inorder(t.left);
yield t.label;
yield* inorder(t.right);
}
}
// 下面遞迴生成二叉樹
function make(array) {
// 判斷是否為葉節點
if (array.length == 1) return new Tree(null, array[0], null);
return new Tree(make(array[0]), array[1], make(array[2]));
}
let tree = make([[['a'], 'b', ['c']], 'd', [['e'], 'f', ['g']]]);
// 遍歷二叉樹
var result = [];
for (let node of inorder(tree)) {
result.push(node);
}
result
// ['a', 'b', 'c', 'd', 'e', 'f', 'g']
作為物件屬性的 Generator 函式
如果一個物件的屬性是 Generator
函式,可以簡寫成下面的形式
let obj = {
* myGeneratorMethod() {
···
}
};
Generator 函式的 this
Generator
函式總是返回一個遍歷器,ES6規定這個遍歷器是Generator
函式的例項,也繼承了 Generator
函式的prototype
物件上的方法。
如果把Generator
當作普通的建構函式,並不會生效,因為其返回的總是遍歷器物件,而不是this
物件。Generator
函式也不能跟new
命令一起用,會報錯
function* g() {
this.a = 11;
}
let obj = g();
obj.next();
obj.a // undefined
將this
物件繫結至Generator
函式的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
在外面包一層普通函式,即可使用new
命令
function* F() {
this.a = 1;
yield this.b = 2;
yield this.c = 3;
}
function F_constructor(){
return F.call(F.prototype)
}
f = new F_constructor();
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
Generator 與上下文
JavaScript 程式碼執行時,會產生一個全域性的上下文環境(context,又稱執行環境),包含了當前所有的變數和物件。然後,執行函式(或塊級程式碼)的時候,又會在當前上下文環境的上層,產生一個函式執行的上下文,變成當前(active)的上下文,由此形成一個上下文環境的堆疊(context stack)。
這個堆疊是“後進先出”的資料結構,最後產生的上下文環境首先執行完成,退出堆疊,然後再執行完成它下層的上下文,直至所有程式碼執行完成,堆疊清空。
Generator
函式不是這樣,它執行產生的上下文環境,一旦遇到yield
命令,就會暫時退出堆疊,但是並不消失,裡面的所有變數和物件會凍結在當前狀態。等到對它執行next
命令時,這個上下文環境又會重新加入呼叫棧,凍結的變數和物件恢復執行。
應用
(1)非同步操作的同步化表示
Generator
函式的暫停執行的效果,意味著可以把非同步操作寫在yield
表示式裡面,等到呼叫next方法時再往後執行。這實際上等同於不需要寫回調函數了,因為非同步操作的後續操作可以放在yield表示式下面,反正要等到呼叫next
方法時再執行。所以,Generator
函式的一個重要實際意義就是用來處理非同步操作,改寫回調函式。
下面是通過 Generator
函式部署 Ajax 操作
function* main() {
var result = yield request("http://some.url"); //等待返回response
var resp = JSON.parse(result);
console.log(resp.value);
}
function request(url) {
makeAjaxCall(url, function(response){
it.next(response); //通過next方法傳遞response
});
}
var it = main();
it.next();
(2)控制流管理
利用for...of
迴圈會自動依次執行yield
命令的特性,提供一種更一般的控制流管理的方法。
let steps = [step1Func, step2Func, step3Func];
function* iterateSteps(steps){
for (var i=0; i< steps.length; i++){
var step = steps[i];
yield step();
}
}
將任務分解成步驟之後,還可以使用yield*
將專案分解成多個依次執行的任務
let jobs = [job1, job2, job3];
function* iterateJobs(jobs){
for (var i=0; i< jobs.length; i++){
var job = jobs[i];
yield* iterateSteps(job.steps);
}
}
最後,就可以用for...of
迴圈一次性依次執行所有任務的所有步驟
for (var step of iterateJobs(jobs)){
console.log(step.id);
}
(3)部署Iterator
介面
利用 Generator
函式,可以在任意物件上部署 Iterator
介面。
(4)作為資料結構
Generator
可以看作是資料結構,更確切地說,可以看作是一個數組結構,因為 Generator
函式可以返回一系列的值,這意味著它可以對任意表達式,提供類似陣列的介面。
function* doStuff() {
yield fs.readFile.bind(null, 'hello.txt');
yield fs.readFile.bind(null, 'world.txt');
yield fs.readFile.bind(null, 'and-such.txt');
}
for (task of doStuff()) {
// task是一個函式,可以像回撥函式那樣使用它
}