在上一篇文章裏《【ES6基礎】迭代器(iterator)》,筆者介紹了迭代器及相關實例,咱們要實現一個迭代器要寫很多的代碼。幸運的是,ES6引入了一個新的函數類型——生成器函數(Generator function),讓咱們可以更輕鬆更便捷的實現迭代器的相關功能。javascript
今天筆者將從如下幾個方面進行介紹生成器(Generator):css
本篇文章閱讀時間預計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——用來標註暫停點,以下段代碼所示:
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* 能夠將可迭代的對象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)方法,隨時終止生成器,以下段代碼所示:
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提早終止了生成器,其餘的值也再也不返回。
除了用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複製代碼
從上述代碼咱們能夠看出:
從上述步驟說明中,向生成器傳遞數據,首行的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函數中咱們能夠看出:
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()函數的代碼更加友好,和同步代碼的感受是一致的,接下來是這樣的:
你是否是發現一個異步調用就和同步調用同樣,但它是以異步的方式運行的。
例如咱們有一個需求,用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》
更多精彩內容,請微信關注」前端達人」公衆號!