從forEach到迭代器

本文從使用 forEach 對數組進行遍歷開始提及,粗略對比使用 forEach , for...in , for...of 進行遍歷的差別,並由此引入 ES6 中 *可迭代對象/迭代器 *的概念,並對其進行粗略介紹。javascript

forEach

forEach 方法按升序爲數組中的有效值的每一項執行一次callback 函數html

特色

  1. 只會對「有效值」進行操做,也就是說遍歷的時候會跳過已刪除或者未初始化的項,即會跳過那些值爲 empty 的內容。(undefined 不會被跳過哦)。

image.png | left | 426x231

  1. forEach 的第二個參數 thisArg 傳入後能夠改變 callback 函數的 this 值,若是不傳則爲 undefined 。固然, callback 所拿到的 this 值也受廣泛規律的支配:這意味着若是 callback 是個箭頭函數,則 thisArg 會被忽略。(由於箭頭函數已經在詞法上綁定了this值,不能再改了)
  2. 不建議在循環中增/刪數組內容:首先,forEach 遍歷的範圍在第一次調用 callback 前就會肯定,這意味着調用forEach 後添加到數組中的項不會被 callback 訪問到。同時,因爲 forEach 的遍歷是基於下標的(能夠這麼理解,並能從 Polyfill 中看到這一實現),那麼在循環中刪了數組幾項內容則會有奇怪的事情發生:好比下圖中,在下標爲 1 的時候執行了 shift() ,那麼原來的第 3 項變爲了第 2 項,原來的第 2 項則被跳過了。

image.png | left | 404x206

缺點

  1. 沒法中斷或跳出:若是想要中斷/跳出,能夠考慮使用以下兩種方式:
    1. 使用 for / for..of
    2. 使用 Array.prototype.every() / Array.prototype.some() 並返回 false / true 來進行中斷/跳出
  2. 沒法鏈式調用(由於返回了 undefined

for...in

for...in 循環實際是爲循環」enumerable「對象而設計的:java

for...in 語句以任意順序遍歷一個對象的 可枚舉屬性 。對於每一個不一樣的屬性,語句都會被執行。es6

特色

  1. 遍歷可枚舉屬性:for...in 循環將遍歷對象自己的全部可枚舉屬性,以及對象從其構造函數原型中繼承的屬性(更接近原型鏈中對象的屬性覆蓋原型屬性)。
  2. 能夠被中斷

缺點

  1. 會訪問到能訪問到的全部的 __ 可枚舉屬性__ ,也就是說會包括那些原型鏈上的屬性。若是想要僅迭代自身的屬性,那麼在使用 for...in 的同時還須要配合 getOwnPropertyNames()hasOwnProperty()
  2. 不能保證for ... in將以任何特定的順序返回索引,好比在 IE 下可能會亂來。
  3. 不建議用於迭代 Array
    1. 不必定保證按照下標順序
    2. 遍歷獲得的下標是字符串而不是數字

for...of

for...of 的本質是在 可迭代對象(iterable objects) 上調用其 迭代方法 建立一個迭代循環,並執行對應語句。能夠迭代 數組/字符串/類型化數組(TypedArray)/Map/Set/generators/類數組對象(NodeList/arguments) 等。須要注意的是, Object 並非一個可迭代對象。數組

for...of 是 ES6 中新出爐的,其彌補了 forEachfor...in 的諸多缺點:瀏覽器

  1. 可使用breakthrow 或return 等終止
  2. 能正常迭代數組(依賴數組默認的迭代行爲)

區別

那麼咱們簡單的幾句話說明一下 for...offor...in 的區別:數據結構

  • for...in 語句以原始插入順序(還不必定保證)迭代對象的 可枚舉屬性
  • for...of 語句遍歷 可迭代對象 定義要迭代的數據。
  • for...of  獲得的是 值(value), 而 for...in 獲得的是 鍵(key)。

那麼扯到了 可迭代對象 ,就不得不說說 ES6 中新增的與 可迭代對象/迭代器 有關東西了。函數

可迭代對象/迭代器

*iteration *是 ES6 中新引入的遍歷數據的機制,其核心概念是:iterable(可迭代對象)  和 iterator(迭代器):ui

  • *iterable(可迭代對象):*一種但願被外界訪問的內部元素的數據結構,實現了 Symbol.iterator 方法
  • *iterator(迭代器):*用於遍歷數據結構元素的指針

可迭代協議(iterable protocol)

首先,可迭代協議容許 JavaScript 對象去定義或定製它們的迭代行爲。而如 Array 或 Map 等內置可迭代對象有默認的迭代行爲,而如 Object 則沒有。(因此爲何不能對 Objectfor...of )再具體一點,即對象或其原型上有 [Symbol.iterator] 的方法,用於返回一個對象的無參函數,被返回對象符合迭代器協議。而後在該對象被迭代的時候,調用其 [Symbol.iterator] 方法得到一個在迭代中使用的迭代器。this

首先能夠看見的是 Array 和 Map 在原型鏈上有該方法,而 Object 則沒有。這印證了上面對於哪些能夠用於 for...of  的說法。

image.png | left | 324x151

若是咱們非要想用 for...ofObject 所擁有的屬性進行遍歷,則可以使用內置的 Object.keys() 方法:

for (const key of Object.keys(someObject)) {
  console.log(key + ": " + someObject[key]);
}
複製代碼

或者若是你想要更簡單得同時獲得鍵和值,能夠考慮使用 Object.entries()

for (const [key, value] of Object.entries(someObject)) {
  console.log(key + ": " + value);
}
複製代碼

其次,有以下狀況會使用可迭代對象的迭代行爲:

  1. for...of
  2. 擴展運算符(Spread syntax)
  3. yield*
  4. 解構賦值(Destructuring assignment )
  5. 一些可接受可迭代對象的內置API(本質是這些接口在接收一個數組做爲參數的時候會調用數組的迭代行爲)
    1. Array.from()
    2. Map(), Set(), WeakMap(), WeakSet()
    3. Promise.all() /Promise.race() 等

迭代器協議(iterator protocol)

上文說到返回了一個迭代器用於迭代,那咱們就來看看符合什麼樣規範的纔算一個 迭代器 。 只須要實現一個符合以下要求的 next 方法的對象便可:

屬性
next
返回一個對象的無參函數,被返回對象擁有兩個屬性:
  • done (boolean)
    • 若是迭代器已經通過了被迭代序列時爲 true。這時 value 可能描述了該迭代器的返回值。
    • 若是迭代器能夠產生序列中的下一個值,則爲 false。這等效於連同 done 屬性也不指定。
  • value - 迭代器返回的任何 JavaScript 值。done 爲 true 時可省略。

本質上,在使用一個迭代器的時候,會不斷調用其 next() 方法直到返回 done: true

自定義迭代行爲

既然符合可迭代協議的均爲可迭代對象,那接下來就簡單自定義一下迭代行爲:

// 讓咱們的數組倒序輸出 value
const myArr = [1, 2, 3];
myArr[Symbol.iterator] = function () {
    const that = this;
    let index = that.length;
    return {
        next: function () {
            if (index > 0) {
                index--;
                return {
                    value: that[index],
                    done: false
                };
            } else {
                return {
                    done: true
                };
            }
        },
    };
};
[...myArr]; // [3, 2, 1]
Array.from(myArr) // [3, 2, 1]
複製代碼

一句說明可迭代對象和迭代器的關係

當一個__可迭代對象__須要被迭代的時候,它的 Symbol.iterator 方法被無參調用,而後返回一個用於在迭代中得到值的迭代器。 換句話說,一個對象(或其原型)上有符合標準的 Symbol.iterator 接口,那他就是 可迭代的(Iterator) ,調用這個接口返回的對象就是一個 迭代器

image.png | left | 657x184

關閉迭代器

上文提到說 for...offorEach 好在其能夠被「中斷」,那麼對於在 for...of 中中斷迭代,其本質是中斷了迭代器,迭代器在中斷後會被關閉。說到這裏,就繼續說一下迭代器關閉的狀況了。

首先,迭代器的關閉分爲兩種狀況:

  1. Exhaustion:當被持續調用 next() 方法直到返回 done: true ,也就是迭代器正常執行完後關閉
  2. Closing:經過調用 return() 方法來告訴迭代器不打算再調用 next() 方法

那麼何時會調用迭代器的 return 方法呢:

  1. 首先,return() 是個可選方法,只有具備該方法的迭代器纔是 可關閉的(closable)
  2. 其次,只有當沒有 Exhaustion 時才應該調用 return() ,如 breakthrow 或 return

最後,return() 方法中也不是想怎麼寫就怎麼寫的,也有本身的要求, return()方法須要符合如下規範:

  1. return(x) 一般應該返回如 { done: true, value: x } 的結果,若是返回的不是個對象則會報錯
  2. 調用return()後, next()返回的對象也應該是 done:true (這就是爲何有一些迭代器在 for...of 循環中中斷後沒法再次使用的緣由,好比 Generator

同時,須要額外注意的是,及時在收到迭代器最後一個值後調用 break 等,也會觸發 return()

function createIterable() {
    let done = false;
    const iterable = {
        [Symbol.iterator]() {
            return this;
        },
        next() {
            if (!done) {
                done = true;
                return {
                    done: false,
                    value: 'a'
                };
            } else {
                return {
                    done: true,
                    value: undefined
                };
            }
        },
        return () {
            console.log('return() was called!');
            return {
                done: true,
                value: undefined
            };
        },
    };
    return iterable;
}
for (const value of createIterable()) {
    console.log(value);
    break;
}
複製代碼

生成器(Generator)

既是迭代器也是可迭代對象

上文 迭代器協議 中提到的返回的擁有 next() 方法的對象和咱們在 Generator 中使用的 next() 方法彷佛如出一轍。確實, Generator 符合可迭代協議和迭代器協議的。

由於 Generator 既有符合規範的 next() (迭代器協議)方法,也有 Symbol.iterator (可迭代協議)方法,所以它 既是迭代器也是可迭代對象

可關閉的(closable

默認狀況下,Generator對象是可關閉的。所以在用 for...of 時中斷迭代後,沒法再次對原有 Generator對象進行迭代。(由於調用return()後, next()返回的對象也應該是 done:true

固然,既然是默認狀況,咱們就能夠想辦法讓其沒法被關閉: 能夠經過包裝一下迭代器,將迭代器自己/原型上的 return() 方法被重寫掉

class PreventReturn {
    constructor(iterator) {
        this.iterator = iterator;
    }
    [Symbol.iterator]() {
        return this;
    }
    next() {
        return this.iterator.next();
    }
    // 重寫掉 return 方法
    return (value = undefined) {
        return {
            done: false,
            value
        };
    }
}
複製代碼

更多關於 Generator 的內容就不在本篇進行闡述,有機會將單獨做爲一篇慢慢講。

參考

相關文章
相關標籤/搜索