ES6學習筆記之Iterator和for...of循環

原文地址:http://es6.ruanyifeng.com/#docs/iterator

Iterator(遍歷器)的概念

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

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

Iterator的作用有三個:一是爲各種數據結構,提供一個統一的、簡便的訪問接口;二是使得數據結構的成員能夠按某種次序排列;三是ES6創造了一種新的遍歷命令for…of循環,Iterator接口主要供for…of消費。

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: false} :
        {value: undefined, done: true};
    }
  };
}

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

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

next方法返回一個對象,表示當前數據成員的信息。這個對象具有value和done兩個屬性,value屬性返回當前位置的成員,done屬性是一個布爾值,表示遍歷是否結束,即是否還有必要再一次調用next方法。

總之,調用指針對象的next方法就可以遍歷事先給定的數據結構。

對於遍歷器對象來說,done: falsevalue: undefined屬性都是可以省略的,因此上面的makeIterator函數可以簡寫成下面的形式。

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

由於Iterator只是把接口規格加到數據結構之上,所以,遍歷器與它所遍歷的那個數據結構,實際上是分開的,完全可以寫出沒有對應數據結構的遍歷器對象,或者說用遍歷器對象模擬出數據結構。下面是一個無限運行的遍歷器對象的例子。

var it = idMaker();

it.next().value // '0'
it.next().value // '1'
it.next().value // '2'
// ...

function idMaker() {
  var index = 0;

  return {
    next: function() {
      return {value: index++, done: false};
    }
  };
}

上面的例子中,遍歷器生成函數idMaker,返回一個遍歷器對象(即指針對象)。但是並沒有對應的數據結構,或者說,遍歷器對象自己描述了一個數據結構出來。

在ES6中,有些數據結構原生具備Iterator接口(比如數組),即不用任何處理,就可以被for…of循環遍歷,有些就不行(比如對象)。原因在於,這些數據結構原生部署了Symbol.iterator屬性(詳見下文),另外一些數據結構沒有。凡是部署了Symbol.iterator屬性的數據結構,就稱爲部署了遍歷器接口。調用這個接口,就會返回一個遍歷器對象。

如果使用TypeScript的寫法,遍歷器接口(Iterable)、指針對象(Iterator)和next方法返回值的規格可以描述如下。

interface Iterable {
  [Symbol.iterator]() : Iterator,
}

interface Iterator {
  next(value?: any) : IterationResult,
}

interface IterationResult {
  value: any,
  done: boolean,
}

數據結構的默認Iterator接口

Iterator接口的目的,就是爲所有數據結構,提供了一種統一的訪問機制,即for…of循環(詳見下文)。當使用for…of循環遍歷某種數據結構時,該循環會自動去尋找Iterator接口。

一種數據結構只要部署了Iterator接口,我們就稱這種數據結構是”可遍歷的“(iterable)。

ES6規定,默認的Iterator接口部署在數據結構的Symbol.iterator屬性,或者說,一個數據結構只要具有Symbol.iterator屬性,就可以認爲是“可遍歷的”(iterable)。Symbol.iterator屬性本身是一個函數,就是當前數據結構默認的遍歷器生成函數。執行這個函數,就會返回一個遍歷器。至於屬性名Symbol.iterator,它是一個表達式,返回Symbol對象的iterator屬性,這是一個預定義好的、類型爲Symbol的特殊值,所以要放在方括號內。(參見Symbol一章)。

const obj = {
  [Symbol.iterator] : function () {
    return {
      next: function () {
        return {
          value: 1,
          done: true
        };
      }
    };
  }
};

上面代碼中,對象obj是可遍歷的(iterable),因爲具有Symbol.iterator屬性。執行這個屬性,會返回一個遍歷器對象。該對象的根本特徵就是具有next方法。每次調用next方法,都會返回一個代表當前成員的信息對象,具有value和done兩個屬性。

在ES6中,有三類數據結構原生具備Iterator接口:數組、某些類似數組的對象、Set和Map結構。

let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();

iter.next() // { value: 'a', done: false }
iter.next() // { value: 'b', done: false }
iter.next() // { value: 'c', done: false }
iter.next() // { value: undefined, done: true }

上面代碼中,變量arr是一個數組,原生就具有遍歷器接口,部署在arr的Symbol.iterator屬性上面。所以,調用這個屬性,就得到遍歷器對象。

上面提到,原生就部署Iterator接口的數據結構有三類,對於這三類數據結構,不用自己寫遍歷器生成函數,for...of循環會自動遍歷它們。除此之外,其他數據結構(主要是對象)的Iterator接口,都需要自己在Symbol.iterator屬性上面部署,這樣纔會被for...of循環遍歷。

對象(Object)之所以沒有默認部署Iterator接口,是因爲對象的哪個屬性先遍歷,哪個屬性後遍歷是不確定的,需要開發者手動指定。本質上,遍歷器是一種線性處理,對於任何非線性的數據結構,部署遍歷器接口,就等於部署一種線性轉換。不過,嚴格地說,對象部署遍歷器接口並不是很必要,因爲這時對象實際上被當作Map結構使用,ES5沒有Map結構,而ES6原生提供了。

一個對象如果要有可被for...of循環調用的Iterator接口,就必須在Symbol.iterator的屬性上部署遍歷器生成方法(原型鏈上的對象具有該方法也可)。

class RangeIterator {
  constructor(start, stop) {
    this.value = start;
    this.stop = stop;
  }

  [Symbol.iterator]() { return this; }

  next() {
    var value = this.value;
    if (value < this.stop) {
      this.value++;
      return {done: false, value: value};
    } else {
      return {done: true, value: undefined};
    }
  }
}

function range(start, stop) {
  return new RangeIterator(start, stop);
}

for (var value of range(0, 3)) {
  console.log(value);
}

上面代碼是一個類部署Iterator接口的寫法。Symbol.iterator屬性對應一個函數,執行後返回當前對象的遍歷器對象。

下面是通過遍歷器實現指針結構的例子。

function Obj(value) {
  this.value = value;
  this.next = null;
}

Obj.prototype[Symbol.iterator] = function() {
  var iterator = {
    next: next
  };

  var current = this;

  function next() {
    if (current) {
      var value = current.value;
      current = current.next;
      return {
        done: false,
        value: value
      };
    } else {
      return {
        done: true
      };
    }
  }
  return iterator;
}

var one = new Obj(1);
var two = new Obj(2);
var three = new Obj(3);

one.next = two;
two.next = three;

for (var i of one){
  console.log(i);
}
// 1
// 2
// 3

上面代碼首先在構造函數的原型鏈上部署Symbol.iterator方法,調用該方法會返回遍歷器對象iterator,調用該對象的next方法,在返回一個值的同時,自動將內部指針移到下一個實例。

下面是另一個爲對象添加Iterator接口的例子。

let obj = {
  data: [ 'hello', 'world' ],
  [Symbol.iterator]() {
    const self = this;
    let index = 0;
    return {
      next() {
        if (index < self.data.length) {
          return {
            value: self.data[index++],
            done: false
          };
        } else {
          return { value: undefined, done: true };
        }
      }
    };
  }
};

對於類似數組的對象(存在數值鍵名和length屬性),部署Iterator接口,有一個簡便方法,就是Symbol.iterator方法直接引用數組的Iterator接口。

NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
// 或者
NodeList.prototype[Symbol.iterator] = [][Symbol.iterator];

[...document.querySelectorAll('div')] // 可以執行了

下面是類似數組的對象調用數組的Symbol.iterator方法的例子。

let iterable = {
  0: 'a',
  1: 'b',
  2: 'c',
  length: 3,
  [Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of iterable) {
  console.log(item); // 'a', 'b', 'c'
}

注意,普通對象部署數組的Symbol.iterator方法,並無效果。

let iterable = {
  a: 'a',
  b: 'b',
  c: 'c',
  length: 3,
  [Symbol.iterator]: Array.prototype[Symbol.iterator]
};
for (let item of iterable) {
  console.log(item); // undefined, undefined, undefined
}

如果Symbol.iterator方法對應的不是遍歷器生成函數(即會返回一個遍歷器對象),解釋引擎將會報錯。

var obj = {};

obj[Symbol.iterator] = () => 1;

[...obj] // TypeError: [] is not a function

上面代碼中,變量obj的Symbol.iterator方法對應的不是遍歷器生成函數,因此報錯。

有了遍歷器接口,數據結構就可以用for…of循環遍歷(詳見下文),也可以使用while循環遍歷。

var $iterator = ITERABLE[Symbol.iterator]();
var $result = $iterator.next();
while (!$result.done) {
  var x = $result.value;
  // ...
  $result = $iterator.next();
}

上面代碼中,ITERABLE代表某種可遍歷的數據結構,$iterator是它的遍歷器對象。遍歷器對象每次移動指針(next方法),都檢查一下返回值的done屬性,如果遍歷還沒結束,就移動遍歷器對象的指針到下一步(next方法),不斷循環。

調用Iterator接口的場合

有一些場合會默認調用Iterator接口(即·Symbol.iterator·方法),除了下文會介紹的for…of循環,還有幾個別的場合。

  • (1)解構賦值

對數組和Set結構進行解構賦值時,會默認調用在Symbol.iterator方法。

let set = new Set().add('a').add('b').add('c');

let [x,y] = set;
// x='a'; y='b'

let [first, ...rest] = set;
// first='a'; rest=['b','c'];
  • (2)擴展運算符

擴展運算符(…)也會調用默認的iterator接口。

// 例一
var str = 'hello';
[...str] //  ['h','e','l','l','o']

// 例二
let arr = ['b', 'c'];
['a', ...arr, 'd']
// ['a', 'b', 'c', 'd']

上面代碼的擴展運算符內部就調用Iterator接口。

實際上,這提供了一種簡便機制,可以將任何部署了Iterator接口的數據結構,轉爲數組。也就是說,只要某個數據結構部署了Iterator接口,就可以對它使用擴展運算符,將其轉爲數組。

let arr = [...iterable];
  • (3)yield*

yield*後面跟的是一個可遍歷的結構,它會調用該結構的遍歷器接口。

let generator = function* () {
  yield 1;
  yield* [2,3,4];
  yield 5;
};

var iterator = generator();

iterator.next() // { value: 1, done: false }
iterator.next() // { value: 2, done: false }
iterator.next() // { value: 3, done: false }
iterator.next() // { value: 4, done: false }
iterator.next() // { value: 5, done: false }
iterator.next() // { value: undefined, done: true }
  • (4)其他場合

由於數組的遍歷會調用遍歷器接口,所以任何接受數組作爲參數的場合,其實都調用了遍歷器接口。下面是一些例子。

for...of
Array.from()
Map(), Set(), WeakMap(), WeakSet()(比如new Map([['a',1],['b',2]]))
Promise.all()
Promise.race()

字符串的Iterator接口

字符串是一個類似數組的對象,也原生具有Iterator接口。

var someString = "hi";
typeof someString[Symbol.iterator]
// "function"

var iterator = someString[Symbol.iterator]();

iterator.next()  // { value: "h", done: false }
iterator.next()  // { value: "i", done: false }
iterator.next()  // { value: undefined, done: true }

上面代碼中,調用Symbol.iterator方法返回一個遍歷器對象,在這個遍歷器上可以調用next方法,實現對於字符串的遍歷。

可以覆蓋原生的Symbol.iterator方法,達到修改遍歷器行爲的目的。

var str = new String("hi");

[...str] // ["h", "i"]

str[Symbol.iterator] = function() {
  return {
    next: function() {
      if (this._first) {
        this._first = false;
        return { value: "bye", done: false };
      } else {
        return { done: true };
      }
    },
    _first: true
  };
};

[...str] // ["bye"]
str // "hi"

上面代碼中,字符串str的Symbol.iterator方法被修改了,所以擴展運算符(…)返回的值變成了bye,而字符串本身還是hi。

Iterator接口與Generator函數

Symbol.itrator方法的最簡單實現,還是使用下一章要介紹的Generator函數。

var myIterable = {};

myIterable[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  yield 3;
};
[...myIterable] // [1, 2, 3]

// 或者採用下面的簡潔寫法

let obj = {
  * [Symbol.iterator]() {
    yield 'hello';
    yield 'world';
  }
};

for (let x of obj) {
  console.log(x);
}
// hello
// world

上面代碼中,Symbol.iterator方法幾乎不用部署任何代碼,只要用yield命令給出每一步的返回值即可。

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

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

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

function readLinesSync(file) {
  return {
    next() {
      if (file.isAtEndOfFile()) {
        file.close();
        return { done: true };
      }
    },
    return() {
      file.close();
      return { done: true };
    },
  };
}

上面代碼中,函數readLinesSync接受一個文件對象作爲參數,返回一個遍歷器對象,其中除了next方法,還部署了return方法。下面,我們讓文件的遍歷提前返回,這樣就會觸發執行return方法。

for (let line of readLinesSync(fileName)) {
  console.log(line);
  break;
}

注意,return方法必須返回一個對象,這是Generator規格決定的。

throw方法主要是配合Generator函數使用,一般的遍歷器對象用不到這個方法。請參閱《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 對象,以及字符串。

數組

數組原生具備iterator接口(即默認部署了Symbol.iterator屬性),for…of循環本質上就是調用這個接口產生的遍歷器,可以用下面的代碼證明。

const arr = [‘red’, ‘green’, ‘blue’];

for(let v of arr) {
console.log(v); // red green blue
}

const obj = {};
obj[Symbol.iterator] = arr[Symbol.iterator].bind(arr);

for(let v of obj) {
console.log(v); // red green blue
}
上面代碼中,空對象obj部署了數組arr的Symbol.iterator屬性,結果obj的for…of循環,產生了與arr完全一樣的結果。

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
});

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

var arr = ['a', 'b', 'c', 'd'];

for (let a in arr) {
  console.log(a); // 0 1 2 3
}

for (let a of arr) {
  console.log(a); // a b c d
}

上面代碼表明,for…in循環讀取鍵名,for…of循環讀取鍵值。如果要通過for…of循環,獲取數組的索引,可以藉助數組實例的entries方法和keys方法,參見《數組的擴展》章節。

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屬性。

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 成員的鍵名和鍵值。

let map = new Map().set('a', 1).set('b', 2);
for (let pair of map) {
  console.log(pair);
}
// ['a', 1]
// ['b', 2]

for (let [key, value] of map) {
  console.log(key + ' : ' + value);
}
// a : 1
// b : 2

計算生成的數據結構

有些數據結構是在現有數據結構的基礎上,計算生成的。比如,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']

類似數組的對象

類似數組的對象包括好幾類。下面是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);
}

對象

對於普通的對象,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 is not iterable

上面代碼表示,對於普通的對象,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

與其他遍歷語法的比較

以數組爲例,JavaScript 提供多種遍歷語法。最原始的寫法就是for循環。

for (var index = 0; index < myArray.length; index++) {
  console.log(myArray[index]);
}

這種寫法比較麻煩,因此數組提供內置的forEach方法。

myArray.forEach(function (value) {
  console.log(value);
});

這種寫法的問題在於,無法中途跳出forEach循環,break命令或return命令都不能奏效。

for…in循環可以遍歷數組的鍵名。

for (var index in myArray) {
  console.log(myArray[index]);
}

for…in循環有幾個缺點。

  • 數組的鍵名是數字,但是for…in循環是以字符串作爲鍵名“0”、“1”、“2”等等。
  • for…in循環不僅遍歷數字鍵名,還會遍歷手動添加的其他鍵,甚至包括原型鏈上的鍵。
  • 某些情況下,for…in循環會以任意順序遍歷鍵名。
    總之,for…in循環主要是爲遍歷對象而設計的,不適用於遍歷數組。

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循環。

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