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

寫在前面

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

先來看一段很常見的代碼:

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 對象等等)上創建一個迭代循環,調用自定義迭代鉤子,併爲每個不同屬性的值執行語句。

iterator

ECMAScript 2015規定了關於迭代的協議,這些協議可以被任何遵循某些約定的對象來實現。如果一個js對象想要能被迭代,那麼這個對象或者其原型鏈對象必須要有一個Symbol.iterator的屬性,這個屬性的值是一個無參函數,返回一個符合迭代器協議的對象。這樣的對象被稱爲符合【可迭代協議】。

typeof Array.prototype[Symbol.iterator] === 'function'; // true
typeof Array.prototype[Symbol.iterator]() === 'object'; // true

數組之所以可以被for...of迭代,就是因爲數組的原型對象上擁有Symbol.iterator屬性,這個屬性返回了一個符合【迭代器協議】的對象。

一個符合【迭代器協議】的對象必須要有一個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 }

我們自己來實現一個可以迭代的對象。

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)返回的。

什麼是生成器函數呢?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中的異步代碼及其寫法有一個更深入,全面的認識。


圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章