本文首發於公衆號:符合預期的CoyPan
後續文章:【JS基礎】從JavaScript中的for...of提及(下) - async和awaitjavascript
先來看一段很常見的代碼:java
const arr = [1, 2, 3]; for(const i of arr) { console.log(i); // 1,2,3 }
上面的代碼中,用for...of來遍歷一個數組。其實這裏說遍歷不太準確,應該是說:for...of語句在可迭代對象(包括 Array,Map,Set,String,TypedArray,arguments 對象等等)上建立一個迭代循環,調用自定義迭代鉤子,併爲每一個不一樣屬性的值執行語句。git
ECMAScript 2015規定了關於迭代的協議,這些協議能夠被任何遵循某些約定的對象來實現。若是一個js對象想要能被迭代,那麼這個對象或者其原型鏈對象必需要有一個Symbol.iterator的屬性,這個屬性的值是一個無參函數,返回一個符合迭代器協議的對象。這樣的對象被稱爲符合【可迭代協議】。github
typeof Array.prototype[Symbol.iterator] === 'function'; // true typeof Array.prototype[Symbol.iterator]() === 'object'; // true
數組之因此能夠被for...of迭代,就是由於數組的原型對象上擁有Symbol.iterator屬性,這個屬性返回了一個符合【迭代器協議】的對象。ajax
一個符合【迭代器協議】的對象必需要有一個next屬性,next屬性也是一個無參函數,返回一個對象,這個對象至少須要有兩個屬性:done, value, 大概長成下面這樣:segmentfault
{ next: function(){ return { done: boolean, // 布爾值,表示迭代是否完成,若是沒有這個屬性,則默認爲false value: any // 迭代器返回的任何javascript值。若是迭代已經完成,value屬性能夠被省略 } } }
依舊來看一下數組:數組
typeof Array.prototype[Symbol.iterator]().next === 'function' // true Array.prototype[Symbol.iterator]().next() // {value: undefined, done: true} const iteratorObj = [1,2,3][Symbol.iterator](); iteratorObj.next(); // { value: 1, done: false } iteratorObj.next(); // { value: 2, done: false } iteratorObj.next(); // { value: 3, done: false } iteratorObj.next(); // { value: undefined, done: true }
咱們本身來實現一個能夠迭代的對象。異步
const myIterator = { [Symbol.iterator]: function() { return { i: 0, next: function() { if(this.i < 2) { return { value: this.i++ , done: false }; } else { return { done: true }; } } } } } for(const item of myIterator) { console.log(item); } // 0 // 1
不光for...of會使用對象的iterator接口,下面這些用法也會默認使用對象的iteretor接口。
(1) 解構賦值 (2) 擴展運算符 (3) yield*async
generator表示一個生成器對象。這個對象符合【可迭代協議】和【迭代器協議】,是由生成器函數(generator function)返回的。函數
什麼是生成器函數呢?MDN上的描述以下:
生成器函數在執行時能暫停,後面又能從暫停處繼續執行。
調用一個生成器函數並不會立刻執行它裏面的語句,而是返回一個這個生成器的 迭代器 (iterator )對象。當這個迭代器的 next() 方法被首次(後續)調用時,其內的語句會執行到第一個(後續)出現yield的位置爲止,yield 後緊跟迭代器要返回的值。或者若是用的是 yield*(多了個星號),則表示將執行權移交給另外一個生成器函數(當前生成器暫停執行)。next()方法返回一個對象,這個對象包含兩個屬性:value 和 done,value 屬性表示本次 yield 表達式的返回值,done 屬性爲布爾類型,表示生成器後續是否還有 yield 語句,即生成器函數是否已經執行完畢並返回。
看下面的例子:
function* gen() { // gen一個生成器函數 yield 1; yield 2; yield 3; } const g = gen(); // g是一個生成器對象,是可迭代的 Object.prototype.toString.call(g) === "[object Generator]" // true g.next(); // { value: 1, done: false } g.next(); // { value: 2, done: false } g.next(); // { value: 3, done: false } g.next(); // { value: undefined, done: true }
由於生成器對象符合可迭代協議和迭代器協議,咱們能夠用for...of來進行迭代。for…of會拿到迭代器返回值的value,也就是說,在迭代generator時,for…of拿到的是yield後面緊跟的那個值。
function* gen2() { yield 'a'; yield 'b'; yield 'c'; } const g2 = gen2(); for(const i of g2) { console.log(i); } // a // b // c
function *gen1(i) { yield i+1; yield i+2; yield *gen2(i+2); // 將執行權移交給gen2 yield i+3; } function *gen2(i) { yield i*2; } const g = gen1(0); g.next(); // { value: 1, done: false } g.next(); // { value: 2, done: false } g.next(); // { value: 4, done: false } g.next(); // { value: 3, done: false } g.next(); // { value: undefined, done: true }
function* gen3() { let a = yield 1; console.log('a:', a); let b = yield a + 1; yield b + 10; } const g = gen3(); g.next(); // { value: 1, done: false } 這個時候,代碼執行到gen3裏第一行等號右邊 g.next(100); // a: 100 , { value: 101, done: false }。代碼執行第一行等號的左邊,咱們傳入了100,這個100會做爲a的值,接着執行第二行的log, 而後執行到第三行等號的右邊。 g.next(); // { value: NaN, done: false }。代碼執行第三行等號的左半部分,因爲咱們沒有傳值,b就是undefined, undefined + 10 就是NaN了。 g.next(); // { value: undefined, done: true }
若是咱們使用for...of來遍歷上述的生成器對象,因爲for…of拿到的是迭代器返回值的value,因此會獲得如下的結果:
function* gen4() { let a = yield 1; let b = yield a + 1; yield b + 10; } const g4 = gen4(); for(const i of g4) { console.log(i); } // 1 // NaN // NaN
下面是一個使用generator和for...of輸出斐波拉契數列的經典例子:
function* fibonacci() { let [prev, curr] = [0, 1]; while(1){ [prev, curr] = [curr, prev + curr]; yield curr; } } for (let n of fibonacci()) { if (n > 100) { break } console.log(n); }
稍微總結一下,generator給了咱們控制暫停代碼執行的能力,咱們能夠本身來控制代碼執行。那是否能夠用generator來寫異步操做呢 ?
一個很常見的場景: 頁面發起一個ajax請求,請求返回後,執行一個回調函數。在這個回調函數裏,咱們使用第一個請求返回的url,再次發起一個ajax請求。(這裏先不考慮使用Promise)
// 咱們先定義發起ajax的函數,這裏用setTimeout模擬一下 function myAjax(url, cb) { setTimeout(function(){ const data = 'ajax返回了'; cb && cb(resData); }, 1000); } // 通常狀況下,要實現需求,通常能夠這樣寫 myAjax('https://xxxx', function(url){ myAjax(url, function(data){ console.log(data); }); });
咱們嘗試用generator的寫法來實現上面的需求.
// 先把ajax函數改造一下, 把url提出來做爲一個參數,而後返回一個只接受回調函數做爲參數的newAjax函數 // 這種只接受回調函數做爲參數的函數被稱爲thunk函數。 function thunkAjax(url) { return function newAjax(cb){ myAjax(url, cb); } } // 咱們定義一個generator function function* gen() { const res1 = yield thunkAjax('http://url1.xxxx'); console.log('res1', res1); const res2 = yield thunkAjax(res1); console.log('res2', res2); } // 實現需求。 const g = gen(); const y1 = g.next(); // y1 = { value: ƒ, done: false }. 這裏的value,就是一個newAjax函數,接受一個回調函數做爲參數 y1.value(url => { // 執行y1.value這個函數,而且傳入了一個回調函數做爲參數 const y2 = g.next(url); // 傳入url做爲參數,最終會賦值給上面代碼中的res1。 y2 = { value: f, done: false } y2.value(data => { g.next(data); // 傳入data做爲參數,會賦值給上面代碼中的res2。至此,迭代也完成了。 }); }); // 最終的輸出爲: // 1s後輸出:res1 ajax返回了 // 1s後輸出:res2 ajax返回了
在上面的代碼中,咱們使用generator實現了依次執行兩個異步操做。上面的代碼看起來是比較複雜的。整個的邏輯在gen這個generator function裏,而後咱們手動執行完了g這個generator。按照上面的代碼,若是咱們想再加入一個ajax請求,須要先修改generator function,而後修改generator的執行邏輯。咱們來實現一個自動的流程,只須要定義好generator,讓它自動執行。
function autoRun(generatorFun) { const generator = generatorFun(); const run = function(data){ const res = generator.next(data); if(res.done) { return; } return res.value(run); } run(); }
這下,咱們就能夠專一於generator function的邏輯了。
function* gen() { const res1 = yield thunkAjax('http://url1.xxxx'); console.log('res1', res1); const res2 = yield thunkAjax(res1); console.log('res2', res2); const res3 = yield thunkAjax(res2); console.log('res3', res3); ... } // 自動執行 autoRun(gen);
著名的 co就是一個自動執行generator的庫。
上面的代碼中,gen函數體內,咱們用同步代碼的寫法,實現了異步操做。能夠看到,用gererator來執行異步操做,在代碼可讀性、可擴展性上面,是頗有優點的。現在,咱們或許會像下面這樣來寫上面的邏輯:
const fn = async function(){ const res1 = await func1; console.log(res1); const res2 = await func2; console.log(res2); ... } fn();
本文從for..of入手,梳理了javascript中的兩個重要概念:iterator和generator。而且介紹了二者在異步操做中的應用。符合預期。下一篇文章中,將介紹async、await,任務隊列的相關內容,但願能對js中的異步代碼及其寫法有一個更深刻,全面的認識。