摘要:這一概念和相對,認為可以在進程函數(shù)外部對其終止運行。對于從其他語言轉(zhuǎn)向的人來說,它看起來很像函數(shù)返回值指針。
本文翻譯自:The Basics Of ES6 Generators
由于個人能力有限,翻譯中難免有紕漏和錯誤,望不吝指正issue
JavaScript ES6(譯者注:ECMAScript 2015)中最令人興奮的特性之一莫過于Generator函數(shù),它是一種全新的函數(shù)類型。它的名字有些奇怪,初見其功能時甚至更會有些陌生。本篇文章旨在解釋其基本工作原理,并幫助你理解為什么Generator將在未來JS中發(fā)揮強大作用。
Generator從運行到完成的工作方式但我們談?wù)揋enerator函數(shù)時,我們首先應(yīng)該注意到的是,從“運行到完成”其和普通的函數(shù)表現(xiàn)有什么不同之處。
不論你是否已經(jīng)意識到,你已經(jīng)潛意識得認為函數(shù)具有一些非常基礎(chǔ)的特性:函數(shù)一旦開始執(zhí)行,那么在其結(jié)束之前,不會執(zhí)行其他JavaScript代碼。
例如:
setTimeout(function(){ console.log("Hello World"); },1); function foo() { // NOTE: don"t ever do crazy long-running loops like this for (var i=0; i<=1E10; i++) { console.log(i); } } foo(); // 0..1E10 // "Hello World"
上面的代碼中,for循環(huán)會執(zhí)行相當長的時間,長于1秒鐘,但是在foo()函數(shù)執(zhí)行的過程中,我們帶有console.log(...)的定時器并不能夠中斷foo()函數(shù)的運行。因此代碼被阻塞,定時器被推入事件循環(huán)的最后,耐心等待foo函數(shù)執(zhí)行完成。
倘若foo()可以被中斷執(zhí)行?它不會給我們的帶來前所未有的浩劫嗎?
函數(shù)可以被中斷對于多線程編程來說確實是一個挑戰(zhàn),但是值得慶幸的是,在JavaScript的世界中我們沒必要為此而擔心,因為JS總是單線程的(在任何時間只有一條命令/函數(shù)被執(zhí)行)。
注意: Web Workers是JavaScript中實現(xiàn)與JS主線程分離的獨立線程機制,總的說來,Web Workers是與JS主線程平行的另外一個線程。在這兒我們并不介紹多線程并發(fā)的一個原因是,主線程和Web Workers線程只能夠通過異步事件進行通信,因此每個線程內(nèi)部從運行到結(jié)束依然遵循一個接一個的事件循環(huán)機制。
運行-停止-運行由于ES6Generators的到來,我們擁有了另外一種類型的函數(shù),這種函數(shù)可以在執(zhí)行的過程中暫停一次或多次,在將來的某個時間繼續(xù)執(zhí)行,并且允許在Generator函數(shù)暫停的過程中運行其他代碼。
如果你曾經(jīng)閱讀過關(guān)于并發(fā)或者多線程編程的資料,那你一定熟悉“協(xié)程”這一概念,“協(xié)程”的意思就是一個進程(就是一個函數(shù))其可以自行選擇終止運行,以便可以和其他代碼“協(xié)作”完成一些功能。這一概念和“preemptive”相對,preemptive認為可以在進程/函數(shù)外部對其終止運行。
根據(jù)ES6 Generator函數(shù)的并發(fā)行為,我們可以認為其是一種“協(xié)程”。在Generator函數(shù)體內(nèi)部,你可以使用yield關(guān)鍵字在函數(shù)內(nèi)部暫停函數(shù)的執(zhí)行,在Generator函數(shù)外部是無法暫停一個Generator函數(shù)執(zhí)行的;每當Generator函數(shù)遇到一個yield關(guān)鍵字就將暫停執(zhí)行。
然后,一旦一個Generator函數(shù)通過yield暫停執(zhí)行,其不能夠自行恢復(fù)執(zhí)行,需要通過外部的控制來重新啟動generator函數(shù),我們將在文章后面部分介紹這是怎么發(fā)生的。
基本上,只要你愿意,一個Generator函數(shù)可以暫停執(zhí)行/重新啟動任意多次。實際上,你可以再Generator函數(shù)內(nèi)部使用無限循環(huán)(比如非著名的while (true) { .. })來使得函數(shù)可以無盡的暫停/重新啟動。然后這在普通的JS程序中卻是瘋狂的行徑,甚至會拋出錯誤。但是Generator函數(shù)卻能夠表現(xiàn)的非常明智,有些時候你確實想利用Generator函數(shù)這種無盡機制。
更為重要的是,暫停/重新啟動不僅僅用于控制Generator函數(shù)執(zhí)行,它也可以在generator函數(shù)內(nèi)部和外部進行雙向的通信。在普通的JavaScript函數(shù)中,你可以通過傳參的形式將數(shù)據(jù)傳入函數(shù)內(nèi)容,在函數(shù)內(nèi)部通過return語句將函數(shù)的返回值傳遞到函數(shù)外部。在generator函數(shù)中,我們通過yield表達式將信息傳遞到外部,然后通過每次重啟generator函數(shù)將其他信息傳遞給generator。
Generator 函數(shù)的語法然我們看看新奇并且令人興奮的generator函數(shù)的語法是怎樣書寫的。
首先,新的函數(shù)聲明語法:
function *foo() { // .. }
發(fā)現(xiàn)*符號沒?顯得有些陌生且有些奇怪。對于從其他語言轉(zhuǎn)向JavaScript的人來說,它看起來很像函數(shù)返回值指針。但是不要被迷惑到了,*只是用于標識generator函數(shù)而已。
你可能會在其他的文章/文檔中看到如下形式書寫generator函數(shù)function* foo(){},而不是這樣function *foo() {}(*號的位置有所不同)。其實兩種形式都是合法的,但是最近我認為后面一種形式更為準確,因此在本篇文章中都是使用后面一種形式。
現(xiàn)在,讓我們來討論下generator函數(shù)的內(nèi)部構(gòu)成吧。在很多方面,generator函數(shù)和普通函數(shù)無異,只有在generator函數(shù)內(nèi)部有一些新的語法。
正如上面已經(jīng)提及,我們最先需要了解的就是yield關(guān)鍵字,yield__被視為“yield表達式”(并不是一條語句),因為當我們重新啟動generator函數(shù)的時候,我們可以傳遞信息到generator函數(shù)內(nèi)部,不論我們傳遞什么進去,都將被視為yield__表達式的運行結(jié)果。
例如:
function *foo() { var x = 1 + (yield "foo"); console.log(x); }
yield "foo"表達式會在generator函數(shù)暫停時把“foo”字符串傳遞到外部。同時,當generator函數(shù)恢復(fù)執(zhí)行的時候,其他的值又會通過其他表達式傳入到函數(shù)里面作為yield表達式的返回值加1最后再將結(jié)果賦值給x變量。
看到generator函數(shù)的雙向通信了嗎?generator函數(shù)將‘’foo‘’字符串傳遞到外部,暫停函數(shù)執(zhí)行,在將來的某個時間點(可能是立即也可能是很長一段時間后),generator會被重啟,并且會傳遞一個值給generator函數(shù),就好像yield關(guān)鍵字就是某種發(fā)送請求獲取值的請求形式。
在任意表達式中,你可以僅使用yield關(guān)鍵字,后面不跟任何表達式或值。在這種情況下,就相當于將undefined通過yield傳遞出去。如下代碼:
// note: `foo(..)` here is NOT a generator!! function foo(x) { console.log("x: " + x); } function *bar() { yield; // just pause foo( yield ); // pause waiting for a parameter to pass into `foo(..)` }Generator 迭代器
“Generator 迭代器”,是不是相當晦澀難懂?
迭代器是一種特殊的行為,準確說是一種設(shè)計模式,當我們通過調(diào)用next()方法去遍歷一組值的集合時,例如,我們通過在長度為5的數(shù)組[1, 2, 3, 4, 5]上面實現(xiàn)了迭代器。當我們第一次調(diào)用next()的時候,會返回1。第二次調(diào)用next()返回2,如此下去,當所有的值都返回后,再次調(diào)用next()將返回null或者false或其他值,這意味著你已經(jīng)遍歷完真?zhèn)€數(shù)組中的值了。
我們是通過和generator迭代器進行交互來在generator函數(shù)外部控制generator函數(shù),這聽起來比起實際上有些復(fù)雜,考慮下面這個愚蠢的(簡單的)例子:
function *foo() { yield 1; yield 2; yield 3; yield 4; yield 5; }
為了遍歷*foo()generator函數(shù)中的所有值,我們首先需要構(gòu)建一個迭代器,我們怎么去構(gòu)建這個迭代器呢?非常簡單!
var it = foo();
如此之簡單,我們僅僅想執(zhí)行普通函數(shù)一樣執(zhí)行g(shù)enerator函數(shù),其將返回一個迭代器,但是generator函數(shù)中的代碼并不會運行。
這似乎有些奇怪,并且增加了你的理解難度。你甚至會停下來思考,問為什么不通過var it = new foo()的形式來執(zhí)行g(shù)enerator函數(shù)呢,這語法后面的原因可能相當復(fù)雜并超出了我們的討論范疇。
好的,現(xiàn)在讓我們開始迭代我們的generator函數(shù),如下:
var message = it.next();
通過上面的語句,yield表達式將1返回到函數(shù)外部,但是返回的值可能比想象中會多一些。
console.log(message); // { value:1, done:false }
在每一調(diào)用next()后,我們實際上從yield表達式的返回值中獲取到了一個對象,這個對象中有value字段,就是yield返回的值,同時還有一個布爾類型的done字段,其用來表示generator函數(shù)是否已經(jīng)執(zhí)行完畢。
然我們把迭代執(zhí)行完成。
console.log( it.next() ); // { value:2, done:false } console.log( it.next() ); // { value:3, done:false } console.log( it.next() ); // { value:4, done:false } console.log( it.next() ); // { value:5, done:false }
有趣的是,當我們獲取到值為5的時候,done字段依然是false。這因為,實際上generator函數(shù)還么有執(zhí)行完全,我們還可以再次調(diào)用next()。如果我們向函數(shù)內(nèi)部傳遞一個值,其將被設(shè)置為yield 5表達式的返回值,只有在這時候,generator函數(shù)才執(zhí)行完全。
代碼如下:
console.log( it.next() ); // { value:undefined, done:true }
所以最終結(jié)果是,我們迭代執(zhí)行完我們的generator函數(shù),但是最終卻沒有結(jié)果(由于我們已經(jīng)執(zhí)行完所有的yield__表達式)。
你可能會想,我能不能在generator函數(shù)中使用return語句,如果我這樣這,返回值會不會在最終的value字段里面呢?
是...
function *foo() { yield 1; return 2; } var it = foo(); console.log( it.next() ); // { value:1, done:false } console.log( it.next() ); // { value:2, done:true }
... 不是.
依賴于generator函數(shù)的最終返回值也許并不是一個最佳實踐,因為當我們通過for--of循環(huán)來迭代generator函數(shù)的時候(如下),最終return的返回值將被丟棄(無視)。
為了完整,讓我們來看一個同時有雙向數(shù)據(jù)通信的generator函數(shù)的例子:
function *foo(x) { var y = 2 * (yield (x + 1)); var z = yield (y / 3); return (x + y + z); } var it = foo( 5 ); // note: not sending anything into `next()` here console.log( it.next() ); // { value:6, done:false } console.log( it.next( 12 ) ); // { value:8, done:false } console.log( it.next( 13 ) ); // { value:42, done:true }
你可以看到,我們依然可以通過foo(5)傳遞參數(shù)(在例子中是x)給generator函數(shù),就像普通函數(shù)一樣,是的參數(shù)x為5.
在第一次執(zhí)行next(..)的時候,我們并沒有傳遞任何值,為什么?因為在generator內(nèi)部并沒有yield表達式來接收我們傳遞的值。
假如我們真的在第一次調(diào)用next(..)的時候傳遞了值進去,也不會帶來什么壞處,它只是將這個傳入的值拋棄而已。ES6表明,generator函數(shù)在這種情況只是忽略了這些沒有被用到的值。(注意:在寫這篇文章的時候,Chrome和FF的每夜版支持這一特性,但是其他瀏覽有可能沒有完全支持這一特性甚至可能會拋出錯誤)(譯者注:文章發(fā)布于2014年)
yield(x + 1)表達式將傳遞值6到外部,在第二次調(diào)用next(12)時候,傳遞12到generator函數(shù)內(nèi)部作為yield(x + 1)表達式的值,因此y被賦值為12 * 2,值為24。接下來,下一條yield(y / 3)(yield (24 / 3))將向外傳遞值8。第三次調(diào)用next(13)傳遞13到generator函數(shù)內(nèi)部,給yield(y / 3)。是的z被設(shè)置為13.
最后,return (x + y + z)就是return (5 + 24 + 13),也就是42將會作為最終的值返回出去。
重新閱讀幾遍上面的實例。最開始有些難以理解。
for..of循環(huán)ES6在語法層面上大力擁抱迭代器模式,提供了for..of循環(huán)來直接支持迭代器的遍歷。
例如:
function *foo() { yield 1; yield 2; yield 3; yield 4; yield 5; return 6; } for (var v of foo()) { console.log( v ); } // 1 2 3 4 5 console.log( v ); // still `5`, not `6` :(
正如你所見,通過調(diào)用foo()生成的迭代器通過for..of循環(huán)來迭代,循環(huán)自動幫你對迭代器進行遍歷迭代,每次迭代返回一個值,直到done: true,只要done: false,每次循環(huán)都將從value屬性上獲取到值賦值給迭代的變量(例子中的v)。一旦當done為true。循環(huán)迭代結(jié)束。(for..of循環(huán)不會對generator函數(shù)最終的return值進行處理)
正如你所看到的,for..of循環(huán)忽略了generator最后的return 6的值,同時,循環(huán)沒有暴露next()出來,因此我們也不能夠向generator函數(shù)內(nèi)傳遞數(shù)據(jù)。
總結(jié)OK,上面是關(guān)于generator函數(shù)的基本用法,如果你依然對generator函數(shù)感到費解,不要擔心,我們所有人在一開始感覺都是那樣的。
我們很自然的想到這一外來的語法對我們實際代碼有什么作用呢?generator函數(shù)有很多作用,我們只是挖掘了其非常粗淺的一部分。在我們發(fā)現(xiàn)generator函數(shù)如此強大之前我們應(yīng)該更加深入的了解它。
在你練習上面代碼片段之后(在Chrome或者FF每夜版本,或者0.11+帶有--harmony的node環(huán)境下),下面的問題也許會浮出水面:(譯者注:現(xiàn)代瀏覽器最新版本都已支持Generator函數(shù))
怎樣處理generator內(nèi)部錯誤?
在generator函數(shù)內(nèi)部怎么調(diào)用其他generator函數(shù)?
異步代碼怎么和generator函數(shù)協(xié)同工作?
這些問題,或者其他的問題都將在隨后的文章中覆蓋,敬請期待。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/84692.html
摘要:前言又稱提供一個全新的迭代器的概念,它允許我們在語言層面上定義一個有限或無限的序列。后者可以被用來幫助我們理解迭代器。但是當我們使用迭代器時,這個問題就迎刃而解了。是中的新語法,用來配合迭代器。這是因為數(shù)組的迭代器只返回其中預(yù)期的元素。 前言 EcmaScript 2015 (又稱ES6)提供一個全新的迭代器的概念,它允許我們在語言層面上定義一個(有限或無限的)序列。 暫時先拋開它...
摘要:同時,迭代器有一個方法來向函數(shù)中暫停處拋出一個錯誤,該錯誤依然可以通過函數(shù)內(nèi)部的模塊進行捕獲處理。 本文翻譯自:Diving Deeper With ES6 Generators 由于個人能力有限,翻譯中難免有紕漏和錯誤,望不吝指正issue ES6 Generators:完整系列 The Basics Of ES6 Generators Diving Deeper With E...
摘要:語法校驗會給出警告當你仍在使用或不通過任何關(guān)鍵字聲明變量時。但是如果腳本中還有其他的普通導(dǎo)出,就會得到非常奇怪的結(jié)果這個坑爹的情況目前還沒有任何好的解決方案。 我在多年前愛上了coffeScript。對于javaScript,我一直保持著深沉的愛,也十分高興得看到node.js的快速發(fā)展,但是作為一個有python背景的程序員,我更喜歡coffeeScript的簡練語法。 在任何一個活...
摘要:是文檔的一種表示結(jié)構(gòu)。這些任務(wù)大部分都是基于它。這個實踐的重點是把你在前端練級攻略第部分中學到的一些東西和結(jié)合起來。一旦你進入框架部分,你將更好地理解并使用它們。到目前為止,你一直在使用進行操作。它是在前端系統(tǒng)像今天這樣復(fù)雜之前編寫的。 本文是 前端練級攻略 第二部分,第一部分請看下面: 前端練級攻略(第一部分) 在第二部分,我們將重點學習 JavaScript 作為一種獨立的語言,如...
摘要:前端日報精選數(shù)組所有全解密原生實現(xiàn)最簡單的圖片懶加載譯如何抓取數(shù)據(jù)中種常見的內(nèi)存泄露陷阱內(nèi)部原理,第一部分基礎(chǔ)渲染前端國際化中文深入理解筆記模塊掘金譯熱的冷的掘金模塊,桌面端的支付請求,和迷津欲有問遮罩層狀態(tài)丟失及解決方案全 2017-08-20 前端日報 精選 JavaScript數(shù)組所有API全解密原生JS實現(xiàn)最簡單的圖片懶加載【譯】React如何抓取數(shù)據(jù)JavaScript 中 ...
閱讀 2532·2021-09-24 10:29
閱讀 3815·2021-09-22 15:46
閱讀 2583·2021-09-04 16:41
閱讀 2988·2019-08-30 15:53
閱讀 1270·2019-08-30 14:24
閱讀 3063·2019-08-30 13:19
閱讀 2180·2019-08-29 14:17
閱讀 3531·2019-08-29 12:55