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