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

CoyPan發表於2019-04-08

寫在前面

本文首發於公眾號:符合預期的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中的非同步程式碼及其寫法有一個更深入,全面的認識。

相關文章