ES6學習筆記(18)之 Generator 函數(shù)的語法

參考: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表達式的語句是如何運行的慎璧?

Screen Shot 2019-09-11 at 3.27.43 PM.png

我本來以為第一次執(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

兩點:

  1. 為什么打印結果沒有6?

一旦next方法的返回對象的done屬性為true五续,for...of循環(huán)就會中止洒敏,且不包含該返回對象,所以上面代碼的return語句返回的6疙驾,不包括在for...of循環(huán)之中凶伙。

  1. 為什么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"
Screen Shot 2019-09-16 at 11.12.42 AM.png

帶 return 的 Generator 函數(shù)

單獨的:


Screen Shot 2019-09-16 at 11.20.35 AM.png

Generator 函數(shù)中調(diào)用帶 return 的 Generator 函數(shù):

Screen Shot 2019-09-16 at 11.27.29 AM.png

解釋:如果被代理的 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
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市鼓黔,隨后出現(xiàn)的幾起案子洼畅,更是在濱河造成了極大的恐慌枫振,老刑警劉巖,帶你破解...
    沈念sama閱讀 222,807評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件源梭,死亡現(xiàn)場離奇詭異稳吮,居然都是意外死亡缎谷,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來列林,“玉大人瑞你,你說我怎么就攤上這事∠3眨” “怎么了者甲?”我有些...
    開封第一講書人閱讀 169,589評論 0 363
  • 文/不壞的土叔 我叫張陵,是天一觀的道長砌创。 經(jīng)常有香客問我虏缸,道長,這世上最難降的妖魔是什么嫩实? 我笑而不...
    開封第一講書人閱讀 60,188評論 1 300
  • 正文 為了忘掉前任刽辙,我火速辦了婚禮,結果婚禮上甲献,老公的妹妹穿的比我還像新娘宰缤。我一直安慰自己,他們只是感情好晃洒,可當我...
    茶點故事閱讀 69,185評論 6 398
  • 文/花漫 我一把揭開白布慨灭。 她就那樣靜靜地躺著,像睡著了一般锥累。 火紅的嫁衣襯著肌膚如雪缘挑。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,785評論 1 314
  • 那天桶略,我揣著相機與錄音语淘,去河邊找鬼。 笑死际歼,一個胖子當著我的面吹牛惶翻,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播鹅心,決...
    沈念sama閱讀 41,220評論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼吕粗,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了旭愧?” 一聲冷哼從身側響起颅筋,我...
    開封第一講書人閱讀 40,167評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎输枯,沒想到半個月后议泵,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,698評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡桃熄,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,767評論 3 343
  • 正文 我和宋清朗相戀三年先口,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,912評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡碉京,死狀恐怖厢汹,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情谐宙,我是刑警寧澤烫葬,帶...
    沈念sama閱讀 36,572評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站卧惜,受9級特大地震影響厘灼,放射性物質發(fā)生泄漏。R本人自食惡果不足惜咽瓷,卻給世界環(huán)境...
    茶點故事閱讀 42,254評論 3 336
  • 文/蒙蒙 一设凹、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧茅姜,春花似錦闪朱、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,746評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至素标,卻和暖如春称诗,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背头遭。 一陣腳步聲響...
    開封第一講書人閱讀 33,859評論 1 274
  • 我被黑心中介騙來泰國打工寓免, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人计维。 一個月前我還...
    沈念sama閱讀 49,359評論 3 379
  • 正文 我出身青樓袜香,卻偏偏與公主長得像,于是被迫代替她去往敵國和親鲫惶。 傳聞我的和親對象是個殘疾皇子蜈首,可洞房花燭夜當晚...
    茶點故事閱讀 45,922評論 2 361

推薦閱讀更多精彩內(nèi)容