ES6_Iterator 和 for...of 循環

一:Iterator(遍歷器)的概念

JavaScript 原有的表示“集合”的數據結構,主要是數組(Array)和對象(Object),ES6 又添加了Map和Set。這樣就有了四種數據集合,用戶還可以組合使用它們,定義自己的數據結構,比如數組的成員是Map,Map的成員是對象。這樣就需要一種統一的接口機制,來處理所有不同的數據結構。

遍歷器(Iterator)就是這樣一種機制。它是一種接口,爲各種不同的數據結構提供統一的訪問機制。任何數據結構只要部署 Iterator 接口,就可以完成遍歷操作(即依次處理該數據結構的所有成員)。

1:原生具備 Iterator 接口的數據結構如下

(1):Array
(2):Map
(3):Set
(4):String
(5):TypedArray
(6):函數的 arguments 對象
(7):NodeList 對象


2:Iterator 的作用有三個

(1):是爲各種數據結構,提供一個統一的、簡便的訪問接口;

(2):是使得數據結構的成員能夠按某種次序排列;

(3):是 ES6 創造了一種新的遍歷命令for...of循環,Iterator 接口主要供for...of消費。


3:Iterator 的遍歷過程

(1)創建一個指針對象,指向當前數據結構的起始位置。也就是說,遍歷器對象本質上,就是一個指針對象。

(2)第一次調用指針對象的next方法,可以將指針指向數據結構的第一個成員。

(3)第二次調用指針對象的next方法,指針就指向數據結構的第二個成員。

(4)不斷調用指針對象的next方法,直到它指向數據結構的結束位置。

每一次調用next方法,都會返回數據結構的當前成員的信息。具體來說,就是返回一個包含value和done兩個屬性的對象。其中,value屬性是當前成員的值,done屬性是一個布爾值,表示遍歷是否結束。

模擬next方法返回值的例子。

var it = makeIterator(['a', 'b']);

it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }

it.next() // { value: undefined, done: true }

function makeIterator(array) {
  var nextIndex = 0;
  return {
    next: function() {
      return nextIndex < array.length ?
        {value: array[nextIndex++]} :
        {done: true};
    }
  };
}

上面代碼定義了一個makeIterator函數,它是一個遍歷器生成函數,作用就是返回一個遍歷器對象。對數組['a', 'b']執行這個函數,就會返回該數組的遍歷器對象(即指針對象)it。

指針對象的next方法,用來移動指針。開始時,指針指向數組的開始位置。然後,每次調用next方法,指針就會指向數組的下一個成員。第一次調用,指向a;第二次調用,指向b。

next方法返回一個對象,表示當前數據成員的信息。這個對象具有value和done兩個屬性,value屬性返回當前位置的成員,done屬性是一個布爾值,表示遍歷是否結束,即是否還有必要再一次調用next方法。總之,調用指針對象的next方法,就可以遍歷事先給定的數據結構。:

4:遍歷器對象的 return(),throw()

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

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

function readLinesSync(file) {
  return {
    [Symbol.iterator]() {
      return {
        next() {
          return { done: false };
        },
        return() {
          file.close();
          return { done: true };
        }
      };
    },
  };
}
上面代碼中,函數readLinesSync接受一個文件對象作爲參數,返回一個遍歷器對象,其中除了next方法,還部署了return方法。
注意:return方法必須返回一個對象,這是 Generator 規格決定的。
throw方法主要是配合 Generator 函數使用,一般的遍歷器對象用不到這個方法


二:for...of 循環

ES6 借鑑 C++、Java、C# 和 Python 語言,引入了for...of循環,作爲遍歷所有數據結構的統一的方法。

一個數據結構只要部署了Symbol.iterator屬性,就被視爲具有 iterator 接口,就可以用for...of循環遍歷它的成員。也就是說,for...of循環內部調用的是數據結構的Symbol.iterator方法

for...of循環可以使用的範圍包括數組、Set 和 Map 結構、某些類似數組的對象(比如arguments對象、DOM NodeList 對象)、後文的 Generator 對象,以及字符串

1:數組

數組原生具備iterator接口(即默認部署了Symbol.iterator屬性),for...of循環本質上就是調用這個接口產生的遍歷器,可以用下面的代碼證明。
const arr = ['red', 'green', 'blue'];
for(let v of arr) {
  console.log(v); // red green blue
}

for...of循環可以代替數組實例的forEach方法。
const arr = ['red', 'green', 'blue'];
arr.forEach(function (element, index) {
  console.log(element); // red green blue
  console.log(index);   // 0 1 2
});


(1)JavaScript 原有的for...in循環,只能獲得對象的鍵名,不能直接獲取鍵值。ES6 提供for...of循環,允許遍歷獲得鍵值。

(2)for...of循環調用遍歷器接口,數組的遍歷器接口只返回具有數字索引的屬性。這一點跟for...in循環也不一樣。

let arr = [3, 5, 7];
arr.foo = 'hello';
for (let i in arr) {
  console.log(i); // "0", "1", "2", "foo"
}

for (let i of arr) {
  console.log(i); //  "3", "5", "7"
}
上面代碼中,for...of循環不會返回數組arr的foo屬性。


(2)Set 和 Map 結構

Set 和 Map 結構也原生具有 Iterator 接口,可以直接使用for...of循環。

var engines = new Set(["Gecko", "Trident", "Webkit", "Webkit"]);
for (var e of engines) {
  console.log(e);
}
// Gecko
// Trident
// Webkit


var es6 = new Map();
es6.set("edition", 6);
es6.set("committee", "TC39");
es6.set("standard", "ECMA-262");
for (var [name, value] of es6) {
  console.log(name + ": " + value);
}
// edition: 6
// committee: TC39

// standard: ECMA-262

上面代碼演示瞭如何遍歷 Set 結構和 Map 結構。值得注意的地方有兩個,首先,遍歷的順序是按照各個成員被添加進數據結構的順序。其次,Set 結構遍歷時,返回的是一個值,而Map 結構遍歷時,返回的是一個數組,該數組的兩個成員分別爲當前 Map 成員的鍵名和鍵值。


(3)計算生成的數據結構

有些數據結構是在現有數據結構的基礎上,計算生成的。比如,ES6 的數組、Set、Map 都部署了以下三個方法,調用後都返回遍歷器對象。

entries() 返回一個遍歷器對象,用來遍歷[鍵名, 鍵值]組成的數組。對於數組,鍵名就是索引值;對於 Set,鍵名與鍵值相同。Map 結構的 Iterator 接口,默認就是調用entries方法。

keys() 返回一個遍歷器對象,用來遍歷所有的鍵名。

values() 返回一個遍歷器對象,用來遍歷所有的鍵值。

這三個方法調用後生成的遍歷器對象,所遍歷的都是計算生成的數據結構。
let arr = ['a', 'b', 'c'];
for (let pair of arr.entries()) {
  console.log(pair);
}
// [0, 'a']
// [1, 'b']
// [2, 'c']


(4)類似數組的對象

並不是所有類似數組的對象都具有 Iterator 接口,一個簡便的解決方法,就是使用Array.from方法將其轉爲數組。

類似數組的對象包括好幾類。下面是for...of循環用於字符串、DOM NodeList 對象、arguments對象的例子。
// 字符串
let str = "hello";
for (let s of str) {
  console.log(s); // h e l l o
}


// DOM NodeList對象
let paras = document.querySelectorAll("p");
for (let p of paras) {
  p.classList.add("test");
}


// arguments對象
function printArgs() {
  for (let x of arguments) {
    console.log(x);
  }
}
printArgs('a', 'b');
// 'a'
// 'b'
對於字符串來說,for...of循環還有一個特點,就是會正確識別 32 位 UTF-16 字符。


for (let x of 'a\uD83D\uDC0A') {
  console.log(x);
}
// 'a'
// '\uD83D\uDC0A'

並不是所有類似數組的對象都具有 Iterator 接口,一個簡便的解決方法,就是使用Array.from方法將其轉爲數組。

let arrayLike = { length: 2, 0: 'a', 1: 'b' };
// 報錯
for (let x of arrayLike) {
  console.log(x);
}
// 正確
for (let x of Array.from(arrayLike)) {
  console.log(x);
}


(5)對象

對於普通的對象,for...of結構不能直接使用,會報錯,必須部署了 Iterator 接口後才能使用。但是,這樣情況下,for...in循環依然可以用來遍歷鍵名。

let es6 = {
  edition: 6,
  committee: "TC39",
  standard: "ECMA-262"
};

for (let e in es6) {
  console.log(e);
}
// edition
// committee
// standard


for (let e of es6) {
  console.log(e);
}
// TypeError: es6[Symbol.iterator] is not a function
上面代碼表示,對於普通的對象,for...in循環可以遍歷鍵名,for...of循環會報錯

一種解決方法是,使用Object.keys方法將對象的鍵名生成一個數組,然後遍歷這個數組。
for (var key of Object.keys(someObject)) {
  console.log(key + ': ' + someObject[key]);
}


另一個方法是使用 Generator 函數將對象重新包裝一下。
function* entries(obj) {
  for (let key of Object.keys(obj)) {
    yield [key, obj[key]];
  }
}
for (let [key, value] of entries(obj)) {
  console.log(key, '->', value);
}
// a -> 1
// b -> 2
// c -> 3


三:for...of與其他遍歷語法的比較

以數組爲例,JavaScript 提供多種遍歷語法。

1:for循環
for (var index = 0; index < myArray.length; index++) {
  console.log(myArray[index]);
}
這種寫法比較麻煩,因此數組提供內置的forEach方法。



2:forEach循環
myArray.forEach(function (value) {
  console.log(value);
});
這種寫法的問題在於,無法中途跳出forEach循環,break命令或return命令都不能奏效。



3:for...in循環可以遍歷數組的鍵名。
for (var index in myArray) {
  console.log(myArray[index]);
}
for...in循環有幾個缺點。
數組的鍵名是數字,但是for...in循環是以字符串作爲鍵名“0”、“1”、“2”等等。
for...in循環不僅遍歷數字鍵名,還會遍歷手動添加的其他鍵,甚至包括原型鏈上的鍵。
某些情況下,for...in循環會以任意順序遍歷鍵名。
總之,for...in循環主要是爲遍歷對象而設計的,不適用於遍歷數組。



4:for...of循環相比上面幾種做法,有一些顯著的優點。

for (let value of myArray) {
  console.log(value);
}
有着同for...in一樣的簡潔語法,但是沒有for...in那些缺點。
不同於forEach方法,它可以與break、continue和return配合使用。
提供了遍歷所有數據結構的統一操作接口。
下面是一個使用 break 語句,跳出for...of循環的例子。

for (var n of fibonacci) {
  if (n > 1000)
    break;
  console.log(n);
}
上面的例子,會輸出斐波納契數列小於等於 1000 的項。如果當前項大於 1000,就會使用break語句跳出for...of循環。



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