ES6 系列之迭代器與 for of

冴羽發表於2018-07-10

起源

一段標準的 for 迴圈程式碼:

var colors = ["red", "green", "blue"];

for (var i = 0, len = colors.length; i < len; i++) {
    console.log(colors[i]);
}
複製程式碼

看著很簡單,但是再回顧這段程式碼,實際上我們僅僅是需要陣列中元素的值,但是卻需要提前獲取陣列長度,宣告索引變數等,尤其當多個迴圈巢狀的時候,更需要使用多個索引變數,程式碼的複雜度就會大大增加,比如我們使用雙重迴圈進行去重:

function unique(array) {
    var res = [];
    for (var i = 0, arrayLen = array.length; i < arrayLen; i++) {
        for (var j = 0, resLen = res.length; j < resLen; j++) {
            if (array[i] === res[j]) {
                break;
            }
        }
        if (j === resLen) {
            res.push(array[i]);
        }
    }
    return res;
}
複製程式碼

為了消除這種複雜度以及減少迴圈中的錯誤(比如錯誤使用其他迴圈中的變數),ES6 提供了迭代器和 for of 迴圈共同解決這個問題。

迭代器

所謂迭代器,其實就是一個具有 next() 方法的物件,每次呼叫 next() 都會返回一個結果物件,該結果物件有兩個屬性,value 表示當前的值,done 表示遍歷是否結束。

我們直接用 ES5 的語法建立一個迭代器:

function createIterator(items) {
    var i = 0;
    return {
        next: function() {
            var done = i >= item.length;
            var value = !done ? items[i++] : undefined;

            return {
                done: done,
                value: value
            };
        }
    };
}

// iterator 就是一個迭代器物件
var iterator = createIterator([1, 2, 3]);

console.log(iterator.next()); // { done: false, value: 1 }
console.log(iterator.next()); // { done: false, value: 2 }
console.log(iterator.next()); // { done: false, value: 3 }
console.log(iterator.next()); // { done: true, value: undefined }
複製程式碼

for of

除了迭代器之外,我們還需要一個可以遍歷迭代器物件的方式,ES6 提供了 for of 語句,我們直接用 for of 遍歷一下我們上節生成的遍歷器物件試試:

var iterator = createIterator([1, 2, 3]);

for (let value of iterator) {
    console.log(value);
}
複製程式碼

結果報錯 TypeError: iterator is not iterable,表明我們生成的 iterator 物件並不是 iterable(可遍歷的)。

那什麼才是可遍歷的呢?

其實一種資料結構只要部署了 Iterator 介面,我們就稱這種資料結構是“可遍歷的”(iterable)。

ES6 規定,預設的 Iterator 介面部署在資料結構的 Symbol.iterator 屬性,或者說,一個資料結構只要具有 Symbol.iterator 屬性,就可以認為是"可遍歷的"(iterable)。

舉個例子:

const obj = {
    value: 1
};

for (value of obj) {
    console.log(value);
}

// TypeError: iterator is not iterable
複製程式碼

我們直接 for of 遍歷一個物件,會報錯,然而如果我們給該物件新增 Symbol.iterator 屬性:

const obj = {
    value: 1
};

obj[Symbol.iterator] = function() {
    return createIterator([1, 2, 3]);
};

for (value of obj) {
    console.log(value);
}

// 1
// 2
// 3
複製程式碼

由此,我們也可以發現 for of 遍歷的其實是物件的 Symbol.iterator 屬性。

預設可遍歷物件

然而如果我們直接遍歷一個陣列物件:

const colors = ["red", "green", "blue"];

for (let color of colors) {
    console.log(color);
}

// red
// green
// blue
複製程式碼

儘管我們沒有手動新增 Symbol.iterator 屬性,還是可以遍歷成功,這是因為 ES6 預設部署了 Symbol.iterator 屬性,當然我們也可以手動修改這個屬性:

var colors = ["red", "green", "blue"];

colors[Symbol.iterator] = function() {
    return createIterator([1, 2, 3]);
};

for (let color of colors) {
    console.log(color);
}

// 1
// 2
// 3
複製程式碼

除了陣列之外,還有一些資料結構預設部署了 Symbol.iterator 屬性。

所以 for...of 迴圈可以使用的範圍包括:

  1. 陣列
  2. Set
  3. Map
  4. 類陣列物件,如 arguments 物件、DOM NodeList 物件
  5. Generator 物件
  6. 字串

模擬實現 for of

其實模擬實現 for of 也比較簡單,基本就是通過 Symbol.iterator 屬性獲取迭代器物件,然後使用 while 遍歷一下:

function forOf(obj, cb) {
    let iterable, result;

    if (typeof obj[Symbol.iterator] !== "function")
        throw new TypeError(result + " is not iterable");
    if (typeof cb !== "function") throw new TypeError("cb must be callable");

    iterable = obj[Symbol.iterator]();

    result = iterable.next();
    while (!result.done) {
        cb(result.value);
        result = iterable.next();
    }
}
複製程式碼

內建迭代器

為了更好的訪問物件中的內容,比如有的時候我們僅需要陣列中的值,但有的時候不僅需要使用值還需要使用索引,ES6 為陣列、Map、Set 集合內建了以下三種迭代器:

  1. entries() 返回一個遍歷器物件,用來遍歷[鍵名, 鍵值]組成的陣列。對於陣列,鍵名就是索引值。
  2. keys() 返回一個遍歷器物件,用來遍歷所有的鍵名。
  3. values() 返回一個遍歷器物件,用來遍歷所有的鍵值。

以陣列為例:

var colors = ["red", "green", "blue"];

for (let index of colors.keys()) {
    console.log(index);
}

// 0
// 1
// 2

for (let color of colors.values()) {
    console.log(color);
}

// red
// green
// blue

for (let item of colors.entries()) {
    console.log(item);
}

// [ 0, "red" ]
// [ 1, "green" ]
// [ 2, "blue" ]
複製程式碼

Map 型別與陣列類似,但是對於 Set 型別需要注意以下:

var colors = new Set(["red", "green", "blue"]);

for (let index of colors.keys()) {
    console.log(index);
}

// red
// green
// blue

for (let color of colors.values()) {
    console.log(color);
}

// red
// green
// blue

for (let item of colors.entries()) {
    console.log(item);
}

// [ "red", "red" ]
// [ "green", "green" ]
// [ "blue", "blue" ]
複製程式碼

Set 型別的 keys() 和 values() 返回的是相同的迭代器,這也意味著在 Set 這種資料結構中鍵名與鍵值相同。

而且每個集合型別都有一個預設的迭代器,在 for-of 迴圈中,如果沒有顯式指定則使用預設的迭代器。陣列和 Set 集合的預設迭代器是 values() 方法,Map 集合的預設迭代器是 entries() 方法。

這也就是為什麼直接 for of 遍歷 Set 和 Map 資料結構,會有不同的資料結構返回:

const values = new Set([1, 2, 3]);

for (let value of values) {
    console.log(value);
}

// 1
// 2
// 3
複製程式碼
const values = new Map([["key1", "value1"], ["key2", "value2"]]);
for (let value of values) {
    console.log(value);
}

// ["key1", "value1"]
// ["key2", "value2"]
複製程式碼

遍歷 Map 資料結構的時候可以順便結合解構賦值:

const valuess = new Map([["key1", "value1"], ["key2", "value2"]]);

for (let [key, value] of valuess) {
    console.log(key + ":" + value);
}

// key1:value1
// key2:value2
複製程式碼

Babel 是如何編譯 for of 的

我們可以在 Babel 的 Try it out 中檢視編譯的結果:

const colors = new Set(["red", "green", "blue"]);

for (let color of colors) {
    console.log(color);
}
複製程式碼

對於這樣一段程式碼,編譯的結果如下:

"use strict";

var colors = new Set(["red", "green", "blue"]);

var _iteratorNormalCompletion = true;
var _didIteratorError = false;
var _iteratorError = undefined;

try {
    for (
        var _iterator = colors[Symbol.iterator](), _step;
        !(_iteratorNormalCompletion = (_step = _iterator.next()).done);
        _iteratorNormalCompletion = true
    ) {
        var color = _step.value;

        console.log(color);
    }
} catch (err) {
    _didIteratorError = true;
    _iteratorError = err;
} finally {
    try {
        if (!_iteratorNormalCompletion && _iterator.return) {
            _iterator.return();
        }
    } finally {
        if (_didIteratorError) {
            throw _iteratorError;
        }
    }
}
複製程式碼

至少由編譯的結果可以看出,使用 for of 迴圈的背後,還是會使用 Symbol.iterator 介面。

而這段編譯的程式碼稍微複雜的地方有兩段,一段是 for 迴圈這裡:

for (
    var _iterator = colors[Symbol.iterator](), _step;
    !(_iteratorNormalCompletion = (_step = _iterator.next()).done);
    _iteratorNormalCompletion = true
) {
    var color = _step.value;
    console.log(color);
}
複製程式碼

跟標準的 for 迴圈寫法有些差別,我們看下 for 語句的語法:

for (initialize; test; increment) statement;
複製程式碼

initialize、test 和 increment 三個表示式之間用分號分割,它們分別負責初始化操作迴圈條件判斷計數器變數的更新

for 語句其實就相當於:

initialize;
while (test) {
    statement;
    increment;
}
複製程式碼

程式碼的邏輯為:先進行初始化,然後每次迴圈執行之前會執行 test 表示式,並判斷表示式的結果來決定是否執行迴圈體,如果 test 計算結果為真值,則執行迴圈體中的 statement。最後,執行 increment 表示式。

而且值得注意的是,其實 for 迴圈中的三個表示式中任意一個都可以被忽略,不過分號還是要寫的。

比如 for(;;),不過這就是一個死迴圈……

比如:

var i = 0,
    len = colors.length;
for (; i < len; i++) {
    console.log(colors[i]);
}
複製程式碼

又比如:

var i = 0,
    len = colors.length;
for (; i < len; ) {
    i++;
}
複製程式碼

然後我們再來看 Babel 編譯的這個 for 迴圈表示式:

for (
    var _iterator = colors[Symbol.iterator](), _step;
    !(_iteratorNormalCompletion = (_step = _iterator.next()).done);
    _iteratorNormalCompletion = true
) {
    var color = _step.value;
    console.log(color);
}
複製程式碼

用 while 的寫法相當於:

var _iterator = colors[Symbol.iterator](),
    _step;
while (!(_iteratorNormalCompletion = (_step = _iterator.next()).done)) {
    var color = _step.value;
    console.log(color);
    _iteratorNormalCompletion = true;
}
複製程式碼

是不是就好懂了很多呢,然後你就會發現,其實 _iteratorNormalCompletion = true 這句是完全沒有必要的……

另外一段稍微複雜的程式碼是:

try {
  ...
} catch (err) {
  ...
} finally {
  try {
    if (!_iteratorNormalCompletion && _iterator.return) {
      _iterator.return();
    }
  } finally {
    ...
  }
}
複製程式碼

因為 _iteratorNormalCompletion = (_step = _iterator.next()).done,所以 _iteratorNormalCompletion 表示的就是是否完成了一次完整的迭代過程,如果沒有正常的迭代完成,並且迭代器有 return 方法時,就會執行該方法。

而之所以這麼做,就要提到迭代器的 return 方法。

引用阮一峰老師的 ECMAScript 6 入門:

遍歷器物件除了具有 next 方法,還可以具有 return 方法和 throw 方法。如果你自己寫遍歷器物件生成函式,那麼 next 方法是必須部署的,return 方法和 throw 方法是否部署是可選的。

return 方法的使用場合是,如果 for...of 迴圈提前退出(通常是因為出錯,或者有 break 語句或 continue 語句),就會呼叫 return 方法。如果一個物件在完成遍歷前,需要清理或釋放資源,就可以部署 return 方法。

我們可以舉個例子:

function createIterator(items) {
    var i = 0;
    return {
        next: function() {
            var done = i >= items.length;
            var value = !done ? items[i++] : undefined;

            return {
                done: done,
                value: value
            };
        },
        return: function() {
            console.log("執行了 return 方法");
            return {
                value: 23333,
                done: true
            };
        }
    };
}

var colors = ["red", "green", "blue"];

var iterator = createIterator([1, 2, 3]);

colors[Symbol.iterator] = function() {
    return iterator;
};

for (let color of colors) {
    if (color == 1) break;
    console.log(color);
}
// 執行了 return 方法
複製程式碼

不過正如你在編譯後的程式碼中看到,僅僅是在有 return 函式的時候執行了 return 函式而已,return 函式中返回的值其實並不生效……

但是你不返回值或者返回一個基本型別的值的話,結果又會報錯……

TypeError: Iterator result undefined is not an object
複製程式碼

這是因為 return 方法必須返回一個物件,而這又是 Generator 規範決定的……

總之如果是在瀏覽器中使用的話,return 函式的返回值其實並不生效 T^T

ES6 系列

ES6 系列目錄地址:github.com/mqyqingfeng…

ES6 系列預計寫二十篇左右,旨在加深 ES6 部分知識點的理解,重點講解塊級作用域、標籤模板、箭頭函式、Symbol、Set、Map 以及 Promise 的模擬實現、模組載入方案、非同步處理等內容。

如果有錯誤或者不嚴謹的地方,請務必給予指正,十分感謝。如果喜歡或者有所啟發,歡迎 star,對作者也是一種鼓勵。

相關文章