ES6 Iterator 和 for...of 循環

Iterator(遍歷器)的概念

遍歷器(Iterator)是一種接口,爲各類不一樣的數據結構提供統一的訪問機制。任何數據結構只要部署Iterator接口,就能夠完成遍歷操做(即依次處理該數據結構的全部成員)。數組

Iterator 的做用有三個:數據結構

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

Iterator 的遍歷過程是這樣的:函數

  • 建立一個指針對象,指向當前數據結構的起始位置。也就是說,遍歷器對象本質上,就是一個指針對象
  • 第一次調用指針對象的next方法,能夠將指針指向數據結構的第一個成員
  • 第二次調用指針對象的next方法,指針就指向數據結構的第二個成員
  • 不斷調用指針對象的next方法,直到它指向數據結構的結束位置。

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

下面是一個模擬next方法返回值的例子:prototype

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。指針

因爲 Iterator 只是把接口規格加到數據結構之上,因此,遍歷器與它所遍歷的那個數據結構,其實是分開的,徹底能夠寫出沒有對應數據結構的遍歷器對象,或者說用遍歷器對象模擬出數據結構。下面是一個無限運行的遍歷器對象的例子:rest

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,返回一個遍歷器對象(即指針對象)。可是並無對應的數據結構,或者說,遍歷器對象本身描述了一個數據結構出來code

若是使用 TypeScript 的寫法,遍歷器接口(Iterable)、指針對象(Iterator)和next方法返回值的規格能夠描述以下:對象

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

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

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

默認 Iterator 接口

當使用for...of循環遍歷某種數據結構時,該循環會自動去尋找 Iterator 接口。一種數據結構只要部署了Iterator接口,咱們就稱這種數據結構是」可遍歷的「(iterable)。接口

ES6 規定,默認的 Iterator接口部署在數據結構的Symbol.iterator屬性,或者說,一個數據結構只要具備Symbol.iterator屬性,就能夠認爲是「可遍歷的」。Symbol.iterator屬性自己是一個函數,就是當前數據結構默認的遍歷器生成函數。執行這個函數,就會返回一個遍歷器。

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

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

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

原生具有 Iterator 接口的數據結構以下:

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函數的 arguments 對象
  • NodeList 對象

下面的例子是數組的Symbol.iterator屬性:

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屬性上面。因此,調用這個屬性,就獲得遍歷器對象。

一個對象若是要具有可被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};
    }
    return {done: true, value: undefined};
  }
}

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

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

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

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

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

[...document.querySelectorAll('div')] // 能夠執行了

NodeList 對象是相似數組的對象,原本就具備遍歷接口,能夠直接遍歷。上面代碼中,咱們將它的遍歷接口改爲數組的Symbol.iterator屬性,能夠看到沒有任何影響。

下面是另外一個相似數組的對象調用數組的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

調用 Iterator 接口的場合

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接口,就能夠對它使用擴展運算符,將其轉爲數組。

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 }

Iterator接口與Generator函數

Symbol.iterator方法的最簡單實現:

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

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

遍歷器對象除了具備next方法,還能夠具備return方法和throw方法。若是你本身寫遍歷器對象生成函數,那麼next方法是必須部署的,return方法和throw方法是否部署是可選的。

return方法的使用場合是,若是for...of循環提早退出(一般是由於出錯,或者有break語句或continue語句),就會調用return方法。若是一個對象在完成遍歷前,須要清理或釋放資源,就能夠部署return方法。

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

上面代碼中,函數readLinesSync接受一個文件對象做爲參數,返回一個遍歷器對象,其中除了next方法,還部署了return方法。下面的三種狀況,都會觸發執行return方法:

// 狀況一
for (let line of readLinesSync(fileName)) {
  console.log(line);
  break;
}

// 狀況二
for (let line of readLinesSync(fileName)) {
  console.log(line);
  continue;
}

// 狀況三
for (let line of readLinesSync(fileName)) {
  console.log(line);
  throw new Error();
}

上面代碼中,狀況一輸出文件的第一行之後,就會執行return方法,關閉這個文件;狀況二輸出全部行之後,執行return方法,關閉該文件;狀況三會在執行return方法關閉文件以後,再拋出錯誤。

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

throw方法主要是配合 Generator 函數使用,通常的遍歷器對象用不到這個方法。

相關文章
相關標籤/搜索