ES6 Generator函式之基本用法(1)
Generator函式之基本用法
(1)基本概念
Generator函式是ES6 提供的一種非同步程式設計解決方案,語法與傳統函式完全不同。
Generator函式與普通函式在寫法上的不同
1.function命令與函式名之間有一個星號(*)。
2.函式體內部使用yield語句定義不同的內部狀態。
Generator函式的呼叫方法
Generator函式的呼叫方法與普通函式一樣,也是在函式名後面加上一個圓括號。但是,呼叫Generator函式後,這個函式並不會執行。返回的是一個指向內部狀態的指標物件,也就是遍歷器物件。
接下來必須呼叫遍歷器物件的next方法,使得指標移向下一個狀態。每次呼叫next方法,內部指標就從函式頭部或上一次停下來的地方開始執行,直到遇到下一條yield語句(或return語句為止)。換言之,Generator函式是分段執行的,yield語句是暫停執行的標記,而next方法可以恢復執行。
function* generator() {
yield "q";
yield "w";
return "e";
}
let a = generator();
console.log(a.next());
//{value:"q",done:false}
console.log(a.next());
//{value:"w",done:false}
console.log(a.next());
//{value:"e",done:true}
console.log(a.next ());
//{value:undefined,done:true}
1.呼叫Generator函式返回一個遍歷器物件,代表Generator函式的內部指標。以後每次呼叫遍歷器物件的next方法,就會返回一個有著value和done兩個屬性的物件。
value表示當前的內部狀態的值,是yield語句後面的表示式的值(如果yield後面沒有值,value屬性的值就是undefined);done屬性是一個布林值,表示是否遍歷結束。
2.如果遇到return語句,則value屬性的值是return語句後面的表示式的值(如果return語句後面沒有值,value屬性的值就是undefined),done屬性的值為true,表示遍歷結束。
3.如果done屬性變為true後繼續呼叫next方法,返回的value屬性的值為undefined,done屬性的值為true。
(2)yield表示式
Generator函式返回的遍歷器物件,只有呼叫next方法才會遍歷下一個內部狀態,所以說Generator函式是一種可以暫停執行的函式。yield就是暫停執行的標誌。
1.遍歷器物件的next方法的執行邏輯:
1.遇到yield表示式,就暫停執行後面的操作,並將緊跟在yield後面的那個表示式的值,作為返回的物件的value屬性值。
2.下一次呼叫next方法時,再繼續往下執行,直到遇到下一個yield表示式。
3.如果沒有再遇到新的yield表示式,就一直執行到函式結束,直到return語句為止,並將return語句後面的表示式的值,作為返回的物件的value屬性值。
4.如果該函式沒有return語句,則返回的物件的value屬性值為undefined。
2.惰性求值
只有呼叫next方法且內部指標指向該語句時才會執行yield語句後面的表示式。
function* generator() {
yield 1 + 2;
}
上面的程式碼中,yield後面的表示式1+2不會立即求值,只會在next方法將指標移到這一句時才會求值。
3.yield語句與return語句
yield表示式與return語句既有相似之處,也有區別。相似之處在於,都能返回緊跟在語句後面的那個表示式的值。區別在於每次遇到yield,函式暫停執行,下一次再從該位置繼續向後執行,而return語句不具備位置記憶的功能。一個函式裡面,只能執行一次(或者說一個)return語句,但是可以執行多次(或者說多個)yield表示式。正常函式只能返回一個值,因為只能執行一次return;Generator 函式可以返回一系列的值,因為可以有任意多個yield。
4.Generator 函式可以不用yield表示式,這時就變成了一個單純的暫緩執行函式
function* f() {
console.log('執行了!')
}
var generator = f();
setTimeout(function () {
generator.next()
}, 2000);
上面程式碼中,函式f如果是普通函式,在為變數generator賦值時就會執行。但是,函式f是一個 Generator 函式,就變成只有呼叫next方法時,函式f才會執行。
5.yield表示式只能用在 Generator 函式裡面
yield表示式如果用在其他函式中就會報錯。
var arr = [1, [[2, 3], 4], [5, 6]];
var flat = function* (a) {
a.forEach(function (item) {
if (typeof item !== 'number') {
yield* flat(item);
} else {
yield item;
}
});
};
for (var f of flat(arr)){
console.log(f);
}
上面的程式碼中,forEach方法的引數是一個普通函式,但是在裡面使用了yield表示式(也使用了yield*表示式),所以會產生句法錯誤。
6.yield表示式如果用在另一個表示式之中必須放在圓括號中
function* generator() {
console.log("ni hao " + (yield 123));
console.log("hello " + (yield 456))
}
let a = generator();
console.log(a.next());
//{value:123 done:false}
console.log(a.next());
// ni hao undefined
//{value:456 done:false}
console.log(a.next());
// hello undefined
//{value:undefined done:true}
console.log(a.next());
//{value:undefined done:true}
yield表示式用作函式的引數或者放在賦值表示式的右邊,可以不加括號。
(3)與Iterator介面的關係
任意一個物件的Symnol.iterator方法等於該物件的遍歷器物件生成函式,呼叫該函式會返回該物件的遍歷器物件。
由於Generator函式就是遍歷器生成函式,因此可以把Generator賦值給物件的Symbol.iterator屬性,從而使得該物件具有Iterator介面。
let demo = {};
demo[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
let a=[...demo];
console.log(a);
//[1,2,3]
for(let item of demo){
console.log(item);
}
//1
//2
//3
Generator 函式執行後,返回一個遍歷器物件。該物件本身也具有Symbol.iterator屬性,執行後返回自身。
function* gen(){
// some code
}
var g = gen();
g[Symbol.iterator]() === g
// true
上面程式碼中,gen是一個 Generator 函式,呼叫它會生成一個遍歷器物件g。它的Symbol.iterator屬性,也是一個遍歷器物件生成函式,執行後返回它自己。
(4)next方法的引數
yield語句本身沒有返回值。next方法可以帶一個引數,這個引數會被當作上
一條yield語句的返回值。
function* generator() {
let b=yield "q";
let a=b/2;
console.log(a);
yield "w";
return "e";
}
let a = generator();
console.log(a.next());
console.log(a.next(2))
//{value:q,done:false}
//1
//{value:w,done:false}
第二次呼叫next方法的時候,傳入了引數2,因為next方法傳入的引數被當作上一條yield語句的返回值,所以這是b的值為2,之後a=b/2,a的值為1,之後打印出1。
通過next方法的引數,可以在Generator函式執行的不同階段從外部向內部注入不同的值,從而可以調整函式的行為。
注意第一次使用next方法時傳遞引數是無效的:因為next方法的引數表示上一條yield語句的返回值,所以第一次使用next方法時傳遞的引數是無效的。
(5)for…of迴圈
for…of迴圈可以自動遍歷Generator函式返回的遍歷器物件,此時不再需要呼叫next方法:
function* generator() {
yield "q";
yield "w";
return "e";
}
for(let item of generator()){
console.log(item)
}
//q
//w
一旦next方法的返回物件的done屬性為true,for…of迴圈就會終止,且不包含該返回物件,所以上面的return語句返回的"e"不包括在for…of迴圈中。
原生的普通物件沒有遍歷介面,無法使用for…of迴圈,通過Generator函式可以為它加上這個介面:
function* objectEntries(obj) {
let propKeys = Reflect.ownKeys(obj);
for (let propKey of propKeys) {
yield [propKey, obj[propKey]];
}
}
let jane = { first: 'Jane', last: 'Doe' };
for (let [key, value] of objectEntries(jane)) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
另外一種寫法:
function* objectEntries() {
let propKeys = Object.keys(this);
for (let propKey of propKeys) {
yield [propKey, this[propKey]];
}
}
let jane = { first: 'Jane', last: 'Doe' };
jane[Symbol.iterator] = objectEntries;
for (let [key, value] of jane) {
console.log(`${key}: ${value}`);
}
// first: Jane
// last: Doe
除了for…of迴圈以外,擴充套件運算子(…)、解構賦值和Array.from方法內部呼叫的,都是遍歷器介面。這意味著,它們都可以將 Generator 函式返回的遍歷器物件,作為引數。
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
(6)Generator.prototype.throw()
1.Generator函式返回的遍歷器物件都有一個throw方法
可以在函式體外丟擲錯誤,然後在Generator函式體內捕獲。同時throw方法可以接受一個引數,這個引數會被catch語句接受。
function* generator() {
try {
yield;
} catch (e) {
console.log("內部捕獲 " + e);
}
}
let i = generator();
i.next();
i.throw("a");
//內部捕獲 a
如果遍歷器物件丟擲兩個錯誤,而Generator函式體內只有一個try/catch語句,則第二次丟擲的錯誤不會被捕獲:
function* generator() {
try {
yield;
} catch (e) {
console.log("內部捕獲 " + e);
}
}
let i = generator();
i.next();
i.throw("a");
i.throw("b");
//內部捕獲 a
//Uncaught b
遍歷器物件i丟擲第一個錯誤,被Generator函式內部的catch語句捕獲並處理,這是catch語句已經執行過了。之後i丟擲第二個錯誤,但是因為Generator函式內部的catch語句已經執行過了,所以不會捕捉到這個錯誤。
如果遍歷器物件有兩個try/catch語句:
function* generator() {
try {
yield;
} catch (e) {
console.log("內部捕獲1 " + e);
}
try {
yield;
} catch (e) {
console.log("內部捕獲2 " + e);
}
}
let i = generator();
i.next();
i.throw("a");
i.throw("b");
//內部捕獲1 a
//內部捕獲2 b
2.注意遍歷器物件的throw方法與全域性throw命令的不同
用遍歷器物件的throw方法丟擲的錯誤才會被Generator函式內部的catch語句捕獲。而全域性的throw命令丟擲的錯誤只能被Genenrator函式體外的catch語句捕獲。
function* generator() {
try {
yield;
} catch (e) {
console.log("內部捕獲 " + e);
}
}
let i = generator();
i.next();
throw("a");
//Uncaught a
function* generator() {
try {
yield;
} catch (e) {
console.log("內部捕獲 " + e);
}
}
let i = generator();
i.next();
i.throw("a");
//內部捕獲 a
try{
throw("b");
}catch(e){
console.log("外部捕獲 "+e)
}
//外部捕獲 b
函式外部的一個try/catch語句也是一次只能捕獲一個錯誤:
try{
throw("a");
throw("b");
}catch(e){
console.log("外部捕獲 "+e)
}
//外部捕獲 a
3.如果Generator函式內部沒有部署try/catch語句,則throw語句丟擲的錯誤將被外部的try/catch語句捕獲
function* generator() {
yield;
}
let i = generator();
i.next();
try {
i.throw("a");
} catch (e) {
console.log("外部捕獲 " + e)
}
//外部捕獲 a
4.throw方法對下一次遍歷的影響
如果Generator函式沒有部署try/catch語句,則呼叫throw方法會使遍歷終止
function* generator() {
yield 123;
yield 456;
yield 789;
}
let i = generator();
console.log(i.next());
//{value:123 done:false}
i.throw("a");
//Uncaught a
如果Generator函式部署了try/catch語句,則呼叫throw方法不影響下一次遍歷
throw方法被捕獲之後會附帶執行下一條yield表示式,也就是會附帶執行一次next方法:
function* generator() {
try{
yield 123;
}catch (e) {
console.log("內部捕獲 "+e)
}
yield 456;
yield 789;
}
let i = generator();
console.log(i.next());
//{value:123 done:false}
console.log(i.throw("a"));
//內部捕獲 a
//{value:456 done:false}
console.log(i.next());
//{value:789 done:false}
console.log(i.next());
//{value:undefined done:true}
(7)Generator.prototype.return()
Generator函式返回的遍歷器物件還有一個return方法,可以返回給定的值,並終結Generator函式的遍歷。
1.return方法帶引數,返回值的value屬性為引數值
function* generator() {
yield 123;
yield 456;
yield 789;
}
let i = generator();
console.log(i.next());
//{value:123 done:false}
console.log(i.return("a"));
//{value:"a" done:true}
console.log(i.next());
//{value:undefined done:true}
遍歷器物件呼叫return方法後,返回值的value屬性就是return方法的引數(這裡是“a”)。同時Generator函式的遍歷終止,返回值的done屬性為true。以後再呼叫next方法,value屬性的值總為undefined,done屬性的值總為true。
2.如果return方法不提供引數,則返回值的value屬性為undefined
function* generator() {
yield 123;
yield 456;
yield 789;
}
let i = generator();
console.log(i.next());
//{value:123 done:false}
console.log(i.return());
//{value:undefined done:true}
console.log(i.next());
//{value:undefined done:true}
3.return方法遇到Generator函式內部有try…finally程式碼塊
如果 Generator 函式內部有try…finally程式碼塊,且正在執行try程式碼塊,那麼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 }
上面程式碼中,呼叫return方法後,就開始執行finally程式碼塊,然後等到finally程式碼塊執行完,再執行return方法。
如果 Generator 函式內部有try…finally程式碼塊,但是現在並沒有執行try程式碼塊,那麼return方法會直接執行,返回的值的value屬性是return方法的引數,done屬性是true。
function* numbers() {
yield 1;
try {
yield 2;
yield 3