【JS基礎】從JavaScript中的for...of提及(上) - iterator 和 generator

寫在前面

本文首發於公衆號:符合預期的CoyPanjavascript

先來看一段很常見的代碼: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

iterator

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, 大概長成下面這樣:數組

{
    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 }
複製代碼

咱們本身來實現一個能夠迭代的對象。async

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*函數

generator

生成器對象和生成器函數

generator表示一個生成器對象。這個對象符合【可迭代協議】和【迭代器協議】,是由生成器函數(generator function)返回的。ui

什麼是生成器函數呢?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來寫異步操做呢 ?

iterator,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中的異步代碼及其寫法有一個更深刻,全面的認識。

相關文章
相關標籤/搜索