摘要:沒有顯示顯示顯示關鍵字迭代器生成器用于馬上退出代碼塊并保留現場,當執行迭代器的函數時,則能從退出點恢復現場并繼續執行下去。迭代器迭代器是一個擁有方法和方法的對象,通過函數不斷執行以關鍵字分割的代碼段,通過函數令分割的代碼段拋出異常。
一、前言
第一次看koajs的示例時,發現該語句 function *(next){...............} ,這是啥啊?于是搜索一下,原來這是就是ES6的新特性Generator Function(生成器函數)。
那什么是生成器函數呢?其實就相當于C#2.0中通過yield關鍵字實現的迭代器的生成器(細節有所不同),那么理解的關鍵就在迭代器和yield關鍵字兩部分了。下面將嘗試從表象出發,逐步對生成器函數及利用它進行異步編程進行淺層的分析理解。
二、表象——語法及基本使用示例:
// 定義生成器函數 function *enumerable(msg){ console.log(msg) var msg1 = yield msg + " after " console.log(msg1) var msg2 = yield msg1 + " after" try{ var msg3 = yield msg2 + "after" console.log("ok") } catch(e){ console.log(e) } console.log(msg2 + " over") } // 初始化迭代器 var enumerator = enumerable("hello") var ret = enumerator.next() // 控制臺顯示 hello,ret的值{value:"hello after",done:false} ret = enumerator.next("world") // 控制臺顯示 world,ret的值{value:"world after",done:false} ret = enumerator.next("game") // 控制臺顯示game,ret的值{value:"game after",done:false} // 拋出異常信息 ret = enumerator.throw(new Error("test")) // 控制臺顯示new Error("test")信息,然后顯示game over。ret的值為{done:true} // for...of語句 enumerator = enumerable("hello") for(ret of enumerator) console.log(JSON.stringify(ret)); // 控制臺依次顯示 // hello // {value:"hello after",done:false} // world // {value:"world after",done:false} // {value:"game after",done:false} // game over // {done:true}1. 生成器語函數定義
function* test(){} function * test(){} function *test(){} test = function* (){} test = function *(){} 普通函數添加*號后則成為了成為了生成器函數了。 Object.prototype.toString.call(test) // 顯示[object GeneratorFunction] 生成器函數的行為與普通函數并不相同,表現為如下3點: 1. 通過new運算符或函數調用的形式調用生成器函數,均會返回一個生成器實例; 2. 通過new運算符或函數調用的形式調用生成器函數,均不會馬上執行函數體的代碼; 3. 必須調用生成器實例的next方法才會執行生成器函數體的代碼。 function *say(msg){ console.log(msg) } var gen = say("hello world") // 沒有顯示hello world console.log(Object.prototype.toString.call(gen)) // 顯示[object Generator] gen.next() // 顯示hello world2、 關鍵字yield——迭代器生成器
用于馬上退出代碼塊并保留現場,當執行迭代器的next函數時,則能從退出點恢復現場并繼續執行下去。下面有2點需要注意:
1. yield后面的表達式將作為迭代器next函數的返回值;
2. 迭代器next函數的入參將作為yield的返回值(有點像運算符)。
3、迭代器(Generator)
迭代器是一個擁有 {value:{}, done:{Boolean}} next([])方法 和 {undefined} throw([*])方法 的對象,通過next函數不斷執行以關鍵字yield分割的代碼段,通過throw函數令yield分割的代碼段拋出異常。
迭代器更多的是指迭代器模式,迭代器模式是指通過一個名為迭代器的對象按一定的規則遍歷集合元素,調用者只需告訴迭代器獲取下一個元素即可,而集合的類型、如何獲取元素等因素均由具體的迭代器自行處理。(又一次地關注點分離!)并且由于迭代器模式可以做到 按需執行/延遲執行 的效果,因此能降低遍歷無限序列時內存/棧溢出的問題,也能作為異步編程模式使用。
模式理解的注意點:
1. 迭代器每次進訪問集合的一個元素,并由調用者發起訪問請求時迭代器才執行下一次訪問操作
2. “按一定的規則”,意味著不一定遍歷集合中所有的元素,并且規則可以內聚到迭代器的具體實現上,也可通過策略模式外移到其他模塊中;
3. “集合”,集合可以是一開始就已經初始化好的有限序列集合(如[1,2,3,4,5,6,7]),也可以是按需生成的無限序列集合(如1到無限大)
4. “集合元素”,可以是整數集合、字符串集合等數據集合,也可以是函數等指令+數據的集合;
若觸過C#、Java等服務端語句的朋友應該對迭代器有一定程度的了解,C#的IEnumrable、IEnumerator和Java的Iterable、Iterator就是跟迭代器相關的接口定義,繼承上述接口的迭代器實現均可以通過foreach或for...in語句作循環操作。
那么這里有2點是要注意的:
1. 迭代器是指設計模式,跟具體的語言無關,因此所有語言均可根據該模式實現具體的迭代器;
2. foreach或for...in語句是語法層面的支持,跟迭代器模式沒有必然聯系。(若語法層面不支持,那函數式編程中的遞歸的效果是一樣的,假如編譯器/解析器支持尾遞歸則更好了,可以JS不支持)
下面我們通過迭代器來實現Python中的range函數,并通過range函數創建一個超大的有限序列正整數集合(直接用數組的話絕有可能導致棧溢出哦!)。
// 迭代器構造函數 var RangeIterator = function(start,end,scan){ this.start = arguments.length >= 2 ? start : 0 this.end = end == undefined ? start : end this.scan = scan || 1 this.idx = this.start } // 向迭代器發起訪問下一個元素的請求 // FF和ES6下迭代器接口規范定義了迭代器必須通過名為next的函數發起訪問下一個元素的請求 RangeIterator.prototype.next = function(){ if (this.idx > this.end) if (!!StopIteration) { throw StopIteration }else{ return void 0 } var ret = this.idx this.idx += this.scan return ret } // Python中的range函數 var range = function(start, end, scan){ var iterator = new RangeIterator(start, end, scan) return { // FF下令for...in語句調用對象的迭代器的接口規范 __iterator__: function(){ return iterator }, // 暴露迭代器的next函數 next: function(){ return iterator.next() }, toString: function(){ // 可能會導致棧溢出 var array = [] for (var i = this.next(); i != void 0; i = this.next()) array.push(i) return array + "" } } } var r = range(1, 100000000000000000000) // FF下 // 參考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Iterators_and_Generators#.E5.AE.9A.E4.B9.89.E8.87.AA.E5.AE.9A.E4.B9.89.E8.BF.AD.E4.BB.A3.E5.99.A8 for(var i in r) console.log(i) // 顯示1到99999999999999999999 // 所有瀏覽器 for (var i = r.next(); i != void 0; i = r.next()) console.log(i) // 顯示1到99999999999999999999
由于JS是單線程運行,并且當UI線程被阻塞N秒后,瀏覽器會詢問是否停止腳本的執行,但上述代碼并不會由于序列過大造成棧溢出的問題。假如預先生成1到99999999999999999999或更大數字的數組,那很有可能造成stack overflow。那是由于迭代器實質為一狀態機,而調用next函數則是觸發狀態的轉換,而狀態機中同一時刻用于存放變量的存儲空間固定,并不會出現無限增長的情況。
四、核心2——yield關鍵字回到關鍵字yield上了,其實yield關鍵字就是以一種更直觀、便捷的方式讓我們創建用于遍歷有限序列集合的迭代器,而yield則用于將生成器函數的代碼切片作為有限序列集合的元素(元素的類型為指令+數據,而不僅僅是數據而已)。下面我們一起看看yield關鍵字是怎樣對代碼切片的吧!
// 定義生成器函數 function *enumerable(msg){ console.log(msg) var msg1 = yield msg + " after " console.log(msg1) var msg2 = yield msg1 + " after" console.log(msg2 + " over") }
上述代碼最終會被解析為下面的代碼:
var enumerable = function(msg){ var state = -1 return { next: function(val){ switch(++state){ case 0: console.log(msg + " after") break case 1: var msg1 = val console.log(msg1 + " after") break case 2: var msg2 = val console.log(msg2 + " over") break } } } }
(注意:上述僅僅簡單的分析,更復雜的情況(條件控制、循環、迭代、異常捕獲處理等)可以參考@趙劼的《人肉反編譯使用關鍵字yield的方法》)
五、異步調用中的應用由于迭代器模式實現 延遲執行/按需執行,因此可作為一種異步編程模式來應用。
var iterator = getArticles("dummy.json") // 開始執行 iterator.next() // 異步任務模型 function getData(src){ setTimeout(function(){ iterator.next({tpl: "tpl.html", name: "fsjohnhuang"}) }, 1000) } function getTpl(tpl){ setTimeout(function(){ iterator.next("hello ${name}") }, 3000) } // 同步任務 function render(data, tpl){ return tpl.replace(/${(w+)}/, function(){ return data[arguments[1]] == void 0 ? arguments[0] : data[arguments[1]] }) } // 主邏輯 function *getAritcles(src){ console.log("begin") var data = yield getData(src) var tpl = yield getTpl(data.tpl) var res = render(data, tpl) console.log(rest) }
主邏輯中異步調用的寫法與同步調用的基本沒差異了,爽了吧!但異步任務模型與生成器函數及其生成的迭代器耦合性太大,還是不太好用。下面我們通過實現了Promises/A+規范的Q來進一步解耦。
若執行引擎不支持關鍵字yield,那么上述代碼不就無法執行了嗎?還是那句話,yield關鍵字其實就是語法糖,最終還是會被解析為一個迭代器。因此我們自行實現一個迭代器也是能實現上述效果的,不過過程會繁瑣很多(若如第2節的示例那樣存在try...catch語句,就繁瑣死了@~@),并且代碼的整潔性、可維護性就全靠攻城獅來保證了。(語法糖從語法層面簡化編程和維護難度,但理解底層的工作原理也十分重要哦!)
六、與Q結合// 異步任務模型 function getData(src){ var deferred = Q.defer() setTimeout(function(){ defer.resolve({tpl: "tpl.html", name: "fsjohnhuang"}) }, 1000) return deferred.promise } function getTpl(tpl){ var deferred = Q.defer() setTimeout(function(){ defer.resolve("hello ${name}") }, 3000) return deferred.promise } // 同步任務 function render(data, tpl){ return tpl.replace(/${(w+)}/, function(){ return data[arguments[1]] == void 0 ? arguments[0] : data[arguments[1]] }) } // 主邏輯 Q.async(function *(){ console.log("begin") var data = yield getData("dummy.json") var tpl = yield getTpl(data.tpl) var res = render(data, tpl) console.log(rest) })
暫未閱讀Q的源代碼,暫不作詳細分析。反正API就這樣用,呵呵!
七、與iPromise結合iPromise是我開發的一個Promises/A+的完整實現,閱讀源碼你會發現它繼承了jQuery.Deferred1.5~2.1、jsDeferred、mmDeferred和Promises/A官網實現示例的精妙設計,并且從v0.0.6開始支持ES6特性GeneratorFunction。使用示例如下:
var getData = function(dataSrc){ return iPromise(function(r){ setTimeout(function(){ r(dataSrc + " has loaded") }, 1000) }) } var getTpl = function(tplSrc){ return iPromise(function(r){ setTimeout(function(){ r(tplStr + " has loaded") }, 2000) }) } var render = function(data, tpl){ throw new Error("OMG!") } iPromise(function *(dataSrc, tplSrc){ try{ var data = yield getData(dataSrc) var tpl = yield getTpl(tplSrc) render(data, tpl) } catch(e){ console.log(e) } console.log("over!") }, "dummyData.json", "dummyTpl.json") /* 結果如下 */ // 等待1秒多顯示 dummyData.json has loaded // 等待2秒多顯示 dummyTpl.json has loaded // 顯示 Error: OMG! // Stack trace: // test10/render/<@file:///home/fsjohnhuang/repos/iPromise/test/v0.0.2.html:190:6 // 顯示 over!
v0.6.0的中通過遞歸來實現,具體如下(https://github.com/fsjohnhuang/iPromise/blob/master/src/iPromise.js#L7...):
// FF下生成器函數的入參必須在創建迭代器時傳遞 // 若第一次調用迭代器的next函數傳遞參數,則會報TypeError: attempt to send 第一個入參值 to newborn generator var iterator = mixin.apply(null, toArray(arguments,1)) var next = function(){ var deferred = iPromise() deferred.resolve.apply(deferred, arguments) return deferred.then(function(){ var yieldReturn = iterator.next.apply(iterator, arguments) if(yieldReturn.done) throw Error("StopIteration") return yieldReturn.value }).then(next, function(e){ iterator.throw(e) }) } deferred.resolve() deferred.then(next)八、總結
Generator Function并不是為異步編程而生,但可以將它結合Promise來實現良好的異步編程模型。本篇內容僅簡單介紹Generator Function及相關的異步編程內容,若有紕漏請各位指正,謝謝!
九、 參考http://huangj.in/765
https://www.imququ.com/post/generator-function-in-es6.html
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/The_Iter...
https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Stat...*
http://www.cnblogs.com/yangecnu/archive/2012/03/17/2402432.html
http://www.cnblogs.com/draem0507/p/3795189.html
http://blog.zhaojie.me/2010/06/code-for-fun-iterator-generator-yield-i...
http://blog.zhaojie.me/2010/06/code-for-fun-iterator-generator-yield-i...
http://blog.zhaojie.me/2010/07/why-java-sucks-and-csharp-rocks-6-yield...
如果您覺得本文的內容有趣就掃一下吧!捐贈互勉!
??
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/85835.html
摘要:采用的生成非波拉契數列提供了原生的支持,語法非常有特色,關鍵字后面緊跟一個星號。的詳細介紹參考官網先看如何用這個黑科技重新實現非波拉契樹立的生成。在這個內部,我們定義了一個無限循環,用于計算非波拉契數列。 程序員面試系列 Java面試系列-webapp文件夾和WebContent文件夾的區別? 程序員面試系列:Spring MVC能響應HTTP請求的原因? Java程序員面試系列-什么...
摘要:前言本文就是簡單介紹下語法編譯后的代碼。如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎,對作者也是一種鼓勵。 前言 本文就是簡單介紹下 Generator 語法編譯后的代碼。 Generator function* helloWorldGenerator() { yield hello; yield world; return ending...
摘要:等價與注意如果構造函數有自己的返回,那么情況有所不同。,定義了的屬性,默認是聲明的函數名,匿名函數是。匿名函數表達式和函數聲明都不會創建匿名作用域。 ECMAScript規范中對Function的文檔描述,我認為是ECMAScript規范中最復雜也是最不好理解的一部分,它涉及到了各方面。光對Function就分了Function Definitions、Arrow Function D...
摘要:下例實現了一個數組的迭代器在中,可迭代數據結構比如數組都必須實現一個名為的方法,該方法返回一個該結構元素的迭代器。原話是還可以傳遞返回值。 前記 按照規劃,明年年中,ECMAScript 6(ES6)就要正式發布了。 最近抽空看了Dr. Axel Rauschmayer的幾篇文章和演講PPT,對新特性有了些了解。 趁沒忘,抓緊記錄下,夾雜自己的感受。 計劃分三部分: 新語法...
摘要:每個任務必須顯式地掛起自己,在任務切換發生時給予它完全的控制。在這些嘗試中,數據經常在任務之間共享。但由于明確的暫停,幾乎沒有風險。 翻譯自 github 概述 什么是generators? 我們可以把generators理解成一段可以暫停并重新開始執行的函數 function* genFunc() { // (A) console.log(First); yi...
閱讀 1864·2023-04-25 14:28
閱讀 1897·2021-11-19 09:40
閱讀 2801·2021-11-17 09:33
閱讀 1388·2021-11-02 14:48
閱讀 1713·2019-08-29 16:36
閱讀 3336·2019-08-29 16:09
閱讀 2922·2019-08-29 14:17
閱讀 2383·2019-08-29 14:07