【ES6基礎】生成器(Generator)

在上一篇文章裏《【ES6基礎】迭代器(iterator)》,筆者介紹了迭代器及相關實例,咱們要實現一個迭代器要寫很多的代碼。幸運的是,ES6引入了一個新的函數類型——生成器函數(Generator function),讓咱們可以更輕鬆更便捷的實現迭代器的相關功能。javascript

今天筆者將從如下幾個方面進行介紹生成器(Generator):css

  • 什麼是生成器
  • 生成器的基本語法
  • yield關鍵字
  • 生成器函數的類型檢測
  • yield*委託
  • return(value)方法
  • throw(exception)方法
  • 向生成器傳遞數據
  • 生成器示例應用

本篇文章閱讀時間預計15分鐘前端

什麼是生成器?

生成器第一次出如今CLU語言中。CLU語言是美國麻省理工大學的Barbara Liskov教授和她的學生們在1974年至1975年間所設計和開發出來的。Python、C#和Ruby等語言都受到其影響,實現了生成器的特性,生成器在CLU和C#語言中被稱爲迭代器(iterator),Ruby語言中稱爲枚舉器(Enumerator)。java

生成器的主要功能是:經過一段程序,持續迭代或枚舉出符合某個公式或算法的有序數列中的元素。這個程序即是用於實現這個公式或算法的,而不須要將目標數列完整寫出。git

在ES6定義的生成器函數有別於普通的函數,生成器能夠在執行當中暫停自身,能夠當即恢復執行也能夠過一段時間以後恢復執行。最大的區別就是它並不像普通函數那樣保證運行到完畢。還有一點就是,在執行當中每次暫停或恢復循環都提供了一個雙向信息傳遞的機會,生成器能夠返回一個值,恢復它的控制代碼也可發回一個值。算法

生成器的基本語法

與普通函數語法的差異,在function關鍵字和函數名直接有個*號,這個*做爲生成器函數的主要標識符,以下所示:編程

function *it(){}複製代碼

*號的位置沒有嚴格規定,只要在中間就行,你能夠這麼寫:json

function *it(){ }
function* it(){ }
function * it(){ }
function*it(){ }複製代碼

筆者以爲*靠近函數名——function *it(){ },看着更爲清晰,選擇哪一種書寫方式徹底憑我的喜愛。數組

調用生成器也十分簡單,就和調用普通函數同樣,好比:bash

it();複製代碼

同時也能夠向生成器函數傳遞參數:

function *it(x,y){ 

}
it(5,10);複製代碼

yield關鍵字

生成器函數中,有一個特殊的新關鍵字:yield——用來標註暫停點,以下段代碼所示:

function *generator_function(){ 
  yield 1; 
  yield 2; 
  yield 3;
}複製代碼

如何運行生成器呢?以下段代碼所示:

let generator = generator_function();
console.log(generator.next().value);//1
console.log(generator.next().value);//2
console.log(generator.next().value);//3
console.log(generator.next().done);//true

generator = generator_function();
let iterable = generator[Symbol.iterator]();
console.log(iterable.next().value);//1
console.log(iterable.next().value);//2
console.log(iterable.next().value);//3
console.log(iterable.next().done);//true複製代碼

從上述代碼咱們能夠看出:咱們能夠在實例化的生成器generator的對象裏直接調用next()方法,同時咱們也能夠調用生成器原型鏈的Symbol.iterator屬性方法調用next(),效果是一致的。咱們每調用一次next()方法,就是順序在對應的yield關鍵字的位置暫停,遵照迭代器協議,返回例如這樣形式的對象: {value:」1″,done:false},直到全部的yield的值消費完爲止,再一次調用next()方法生成器函數會返回 {value:undefined,done:true},說明生成器的全部值已消費完。因而可知done屬性用來標識生成器序列是否消費完了。當done屬性爲true時,咱們就應該中止調用生成器實例的next方法。還有一點須要說明帶有yield的生成器都是以惰性求值的順序執行,當咱們須要時,對應的值纔會被計算出來。

生成器函數的類型檢測

如何檢測一個函數是生成器函數和生成器實例的原型呢,咱們可使用constructor.prototype屬性檢測,實例代碼以下:

function *genFn() {}
const gen=genFn();
console.log(genFn.constructor.prototype);//GeneratorFunction {}
console.log(gen.constructor.prototype);//Object [Generator] {}
console.log(gen instanceof genFn)//true
//判斷某個對象是否爲指定生成函數所對應的實例複製代碼

除了以上方法進行判斷,咱們還可使用@@tostringTag屬性,以下段代碼所示:

function *genFn() {}
const gen=genFn();
console.log(genFn[Symbol.toStringTag]);//GeneratorFunction
console.log(gen[Symbol.toStringTag]);//Generator複製代碼

yield*委託

yield* 能夠將可迭代的對象iterable放在一個生成器裏,生成器函數運行到yield * 位置時,將控制權委託給這個迭代器,直到耗盡爲止,以下段代碼所示:

function *generator_function_1(){ 
 yield 2; 
 yield 3;
}
function *generator_function_2(){
 yield 1; 
 yield* generator_function_1(); 
 yield* [4, 5];
}
const generator = generator_function_2();
console.log(generator.next().value); //1
console.log(generator.next().value); //2
console.log(generator.next().value); //3
console.log(generator.next().value); //4
console.log(generator.next().value); //5
console.log(generator.next().done);  //true複製代碼

從上述代碼中,咱們在一個生成器中嵌套了一個生成器和一個數組,當程序運行至生成器generator_function_1()時,將其中的值消費完跳出後,再去迭代消費數組,消費完後,done的屬性值返回true。

return(value)方法

你能夠在生成器裏使用return(value)方法,隨時終止生成器,以下段代碼所示:

function *generator_function(){ 
 yield 1; 
 yield 2; 
 yield 3;
}
const generator = generator_function();
console.log(generator.next().value); //1
console.log(generator.return(22).value); //22
console.log(generator.next().done);//true複製代碼

從上述代碼咱們看出,使用return()方法咱們提早終止了生成器,返回return裏的值,再次調用next()方法時,done屬性的值爲true,因而可知return提早終止了生成器,其餘的值也再也不返回。

throw(exception)方法

除了用return(value)方法能夠終止生成生成器,咱們還能夠調用 throw(exception) 進行提早終止生成器,示例代碼以下:

function *generator_function(){ 
    yield 1;
    yield 2;
    yield 3;    
}
const generator = generator_function();
console.log(generator.next());
try{
    generator.throw("wow");
}
catch(err){
    console.log(err);
}
finally{
    console.log("clean")
}
console.log(generator.next());複製代碼

上段代碼輸出:

{ value: 1, done: false }
wow
clean
{ value: undefined, done: true }複製代碼

由此能夠看出,在生成器外部調用try…catch…finally,throw()異常被try…catch捕捉並返回,並執行了finally代碼塊中的代碼,再次調用next方法,done屬性返回true,說明生成器已被終止,提早消費完畢。

咱們不只能夠在next執行過程當中插入throw()語句,咱們還能夠在生成器內部插入try…catch進行錯誤處理,代碼以下所示:

function *generator_function(){ 
try { 
 yield 1; 
} catch(e) { 
 console.log("1st Exception"); 
} 
try { 
 yield 2; 
} catch(e) { 
 console.log("2nd Exception"); 
}
}
const generator = generator_function();
console.log(generator.next().value);
console.log(generator.throw("exception string").value);
console.log(generator.throw("exception string").done);複製代碼

運行上段代碼將會輸出:

1
1st Exception
2
2nd Exception
true複製代碼

從代碼輸出能夠輸出,當咱們在generator.throw()方法時,被生成器內部上個暫停點的異常處理代碼所捕獲,同時能夠繼續返回下個暫停點的值。因而可知在生成器內部使用try…catch能夠捕獲異常,並不影響值的下次消費,遇到異常不會終止。

向生成器傳遞數據

生成器不但能對外輸出數據,同時咱們也能夠向生成器內部傳遞數據,是否是很神奇呢,仍是從一段代碼開始提及:

function *generator_function(){ 
    const a = yield 12;
    const b = yield a + 1;
    const c = yield b + 2; 
    yield c + 3; // Final Line
}
const generator = generator_function();
console.log(generator.next().value);
console.log(generator.next(5).value);
console.log(generator.next(11).value);
console.log(generator.next(78).value);
console.log(generator.next().done);複製代碼

運行上述代碼將會輸出:

12
6
13
81
true複製代碼

從上述代碼咱們能夠看出:

  • 第一次調用generator.next(),調用yield 12,並返回值12,至關啓動生成器。並在 yield 12 處暫停。
  • 第二次調用咱們向其進行傳值generator.next(5),前一個yield 12這行暫停點獲取傳值,並將5賦值給a, 忽略12這個值,而後運行至 yield (a + 1) 這個暫停點,所以是6,並返回給value屬性。並在 yield a + 1 這行暫停。
  • 第三次調用next,同理在第二處暫停進行恢復復,把11的值賦值給b,忽略a+1運算,所以在yield b + 2中,返回13,並在此行暫停。
  • 第四次調用next,函數運行到最後一行,C變量被賦值78,最後一行爲加法運算,所以value屬性返回81。
  • 再次運行next()方法,done屬性返回true,生成器數值消費完畢。

從上述步驟說明中,向生成器傳遞數據,首行的next方法是啓動生成器,即便向其傳值,也不能進行變量賦值,你能夠拿上述例子進行實驗,不管你傳遞什麼都是徒勞的,由於傳遞數據只能向上個暫停點進行傳遞,首個暫停點不存在上個暫停點。

生成器示例應用

瞭解生成器的基礎知識後,咱們來一塊兒作些有趣的練習:

斐波那契數列

首先咱們實現一個生成斐波那契數列的生成器函數,而後編寫一個輔助函數用於進行控制輸出指定位置的數,以下段代碼所示:

function *fibonacciSequence() {  
    let x = 0, y = 1;  
    for(;;) {   
         yield y;    
        [x, y] = [y, x+y]; 
}}

function fibonacci(n) {  
    for(let f of fibonacciSequence()){    
        if (n-- <= 0) return f; }} console.log(fibonacci(20)) // => 10946複製代碼

此函數只能返回指定位置的數值,若是返回指定位置的數列看起來會更加實用,以下段代碼所示:

function *fibonacciSequence() {  
    let x = 0, y = 1;  
    for(;;) {   
         yield y;    
        [x, y] = [y, x+y]; 
}}

function* take(n, iterable) {  
    let it = iterable[Symbol.iterator](); 
      while(n-- > 0) {        
            let next = it.next();  
    if (next.done){
        return;
    }    
    else { 
        yield next.value
    }; 
}}

console.log([...take(5, fibonacciSequence())])
//[ 1, 1, 2, 3, 5 ]複製代碼

多個生成器進行交錯迭代

好比咱們要實現一個zip函數功能,相似Python的zip函數功能,將多個可迭代的對象合成一個新對象,合成對象的方法,就是循環依次從各個對象的位置進行取值合併,好比有兩個數組a=[1,2,3],b=[4,5,6],合併後就是c=[1,4,2,5,3,6],如何用生成器進行實現呢?以下段代碼所示:

function *oneDigitPrimes() { 
    yield 2;                   
    yield 3;               
    yield 5;                 
    yield 7;           
}
function *zip(...iterables) {  
    let iterators = iterables.map(i => i[Symbol.iterator]()); 
    let index = 0;  
    while(iterators.length > 0) { 
        if (index >= iterators.length)     
        index = 0;                       
        let item = iterators[index].next();   
        if (item.done) {                       
            iterators.splice(index, 1);      
            }
            else {
                yield item.value;                
                index++;
                }  
        }
}
console.log([...zip(oneDigitPrimes(),"ab",[0])]);
//[ 2, 'a', 0, 3, 'b', 5, 7 ]複製代碼

從zip函數中咱們能夠看出:

  • 首先經過Map函數將傳入的可迭代對象進行實例化。
  • 而後循環可迭代對象,經過yield關鍵字調用next()方法進行返回輸出。
  • 直到對應生成器數值消費完畢,移除對應的生成器(迭代器)對象。
  • 直到全部的生成器函數數值消費完,循環迭代的對象爲空,函數中止執行。

經過向後追加的形式合併可迭代對象成一個新對象

function* oneDigitPrimes() { 
    yield 2;                   
    yield 3;               
    yield 5;                 
    yield 7;           
}
function* sequence(...iterables) {  
    for(let iterable of iterables) {   
         yield* iterable;  
        }}
console.log([...sequence("abc",oneDigitPrimes())])
//[ 'a', 'b', 'c', 2, 3, 5, 7 ]複製代碼

使用生成器處理異步調用

假設有兩個簡單的異步函數

let getDataOne=(cb)=>{
    setTimeout(function () {
        cb('response data one');
    }, 1000);
};
let getDateTwo=(cb)=>{
    setTimeout(function () {
        cb('response data two')
    }, 1000)
}複製代碼

將上述代碼改爲使用Generator,咱們使用next(value)的方法向生成器內部傳值,代碼以下:

let generator;
let getDataOne=()=>{
    setTimeout(function () {
        generator.next('response data one');
    }, 1000);
};
let getDateTwo=()=>{
    setTimeout(function () {
        generator.next('response data two')
    }, 1000)
}複製代碼

接下來咱們來實現一個生成器函數main,調用上述方法,代碼以下:

function *main() {
    let dataOne=yield getDataOne();
    let dataTwo=yield getDateTwo();
    console.log("data one",dataOne);
    console.log("data two",dataTwo);
}複製代碼

怎麼運行代碼呢,其實很簡單,以下所示:

generator=main();
generator.next();
//output
//data one response data one
//data two response data two複製代碼

結果按照咱們的預期進行輸出,並且main()函數的代碼更加友好,和同步代碼的感受是一致的,接下來是這樣的:

  • 首先實例化生成器對象
  • 接下來咱們調用next()方法,啓動生成器,生成器在第一行暫停,觸發調用getDataOne()函數。
  • getDataOne()函數在1秒鐘後,觸發調用generator.next(‘response data one’),向生成器main內部變量dataOne傳值,而後在yield getDateTwo()此處暫停,觸發調用getDateTwo()。
  • getDateTwo()函數在1秒鐘後,觸發調用generator.next(‘response data two’),向生成器main內部變量dataTwo傳值,而後運行下面console.log的內容,輸出dataOne,dataTwo變量的值。

你是否是發現一個異步調用就和同步調用同樣,但它是以異步的方式運行的。

一個真實的異步例子

例如咱們有一個需求,用NodeJs實現從論壇帖子列表數據中顯示其中的一個帖子的信息及留言列表信息,代碼以下:

DB/posts.json(帖子列表數據)

[
    {
        "id": "001",
        "title": "Greeting",
        "text": "Hello World",
        "author": "Jane Doe"
    },
    {
        "id": "002",
        "title": "JavaScript 101",
        "text": "The fundamentals of programming.",
        "author": "Alberta Williams"
    },
    {
        "id": "003",
        "title": "Async Programming",
        "text": "Callbacks, Promises and Async/Await.",
        "author": "Alberta Williams"
    }
]複製代碼

DB/comments.json(評論列表)

[
    {
        "id": "phx732",
        "postId": "003",
        "text": "I don't get this callback stuff."
    },
    {
        "id": "avj9438",
        "postId": "003",
        "text": "This is really useful info."
    },
    {
        "id": "gnk368",
        "postId": "001",
        "text": "This is a test comment."
    }
]複製代碼

用回調的方法實現代碼以下 index.js

const fs = require('fs');
const path = require('path');
const postsUrl = path.join(__dirname, 'db/posts.json');
const commentsUrl = path.join(__dirname, 'db/comments.json');
//return the data from our file
function loadCollection(url, callback) {
    fs.readFile(url, 'utf8', function(error, data) {
        if (error) {
            console.log(error);
        } else {
            return callback(JSON.parse(data));
        }
    });
}
//return an object by id
function getRecord(collection, id, callback) {
    var collectobj=collection.find(function(element){
        return element.id == id;
    });
    callback(collectobj);
    return collectobj;
}
//return an array of comments for a post
function getCommentsByPost(comments, postId) {
    return comments.filter(function(comment){
        return comment.postId == postId;
    });
}
loadCollection(postsUrl, function(posts){
    loadCollection(commentsUrl, function(comments){
        getRecord(posts, "001", function(post){
            const postComments = getCommentsByPost(comments, post.id);
            console.log(post);
            console.log(postComments);
        });
    });
});複製代碼

若是用生成器的方法如何實現呢?首先咱們改寫loadCollection方法,代碼以下:

let generator;
function loadCollection(url) {
    fs.readFile(url, 'utf8', function(error, data) {
        if (error) {
            generator.throw(error);
        } else {
            generator.next(JSON.parse(data));
        }
    });
}複製代碼

接着咱們完成main generator 函數的實現,代碼以下:

function *main() {
    let posts=yield loadCollection(postsUrl);
    let comments=yield loadCollection(commentsUrl);
    getRecord(posts, "001", function(post){
                const postComments = getCommentsByPost(comments, post.id);
                console.log(post);
                console.log(postComments);
            });
}複製代碼

最後咱們進行調用

generator=main();
main().next();複製代碼

將一個回調機制轉換成一個生成器函數,看起來是否是很簡潔易懂呢,咱們很輕鬆的建立了看似同步的異步代碼。

小節

關於生成器(Generator)的介紹就到這裏,它能夠經過next方法暫停和恢復執行的函數。next方法還具有向生成器傳遞數據的功能,正是得益這個特色,才能幫助咱們解決異步代碼的問題,讓咱們建立了看似同步的異步代碼,對於咱們來講這個神器是否是特別的強大。

注:本文參考《javascript ES6 函數式編程入門經典》、《你不知道的javascript》、《JavaScript: The Definitive Guide, 7th Edition》

更多精彩內容,請微信關注」前端達人」公衆號!

相關文章
相關標籤/搜索