Iterator 和 for...of 循環(huán)

  1. Iterator(遍歷器)的概念
  2. 默認(rèn) Iterator 接口
  3. 調(diào)用 Iterator 接口的場(chǎng)合
  4. 字符串的 Iterator 接口
  5. Iterator 接口與 Generator 函數(shù)
  6. 遍歷器對(duì)象的 return(),throw()
  7. for...of 循環(huán)

Iterator(遍歷器)的概念 § ?

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

遍歷器(Iterator)就是這樣一種機(jī)制。它是一種接口,為各種不同的數(shù)據(jù)結(jié)構(gòu)提供統(tǒng)一的訪問機(jī)制。任何數(shù)據(jù)結(jié)構(gòu)只要部署 Iterator 接口,就可以完成遍歷操作(即依次處理該數(shù)據(jù)結(jié)構(gòu)的所有成員)。

Iterator 的作用有三個(gè):一是為各種數(shù)據(jù)結(jié)構(gòu),提供一個(gè)統(tǒng)一的、簡(jiǎn)便的訪問接口;二是使得數(shù)據(jù)結(jié)構(gòu)的成員能夠按某種次序排列;三是 ES6 創(chuàng)造了一種新的遍歷命令for...of循環(huán),Iterator 接口主要供for...of消費(fèi)。

Iterator 的遍歷過程是這樣的。

(1)創(chuàng)建一個(gè)指針對(duì)象,指向當(dāng)前數(shù)據(jù)結(jié)構(gòu)的起始位置。也就是說,遍歷器對(duì)象本質(zhì)上,就是一個(gè)指針對(duì)象。

(2)第一次調(diào)用指針對(duì)象的next方法,可以將指針指向數(shù)據(jù)結(jié)構(gòu)的第一個(gè)成員。

(3)第二次調(diào)用指針對(duì)象的next方法,指針就指向數(shù)據(jù)結(jié)構(gòu)的第二個(gè)成員。

(4)不斷調(diào)用指針對(duì)象的next方法,直到它指向數(shù)據(jù)結(jié)構(gòu)的結(jié)束位置。

每一次調(diào)用next方法,都會(huì)返回?cái)?shù)據(jù)結(jié)構(gòu)的當(dāng)前成員的信息。具體來說,就是返回一個(gè)包含value和done兩個(gè)屬性的對(duì)象。其中,value屬性是當(dāng)前成員的值,done屬性是一個(gè)布爾值,表示遍歷是否結(jié)束。

next方法返回一個(gè)對(duì)象,表示當(dāng)前數(shù)據(jù)成員的信息。這個(gè)對(duì)象具有value和done兩個(gè)屬性,value屬性返回當(dāng)前位置的成員,done屬性是一個(gè)布爾值,表示遍歷是否結(jié)束,即是否還有必要再一次調(diào)用next方法。

總之,調(diào)用指針對(duì)象的next方法,就可以遍歷事先給定的數(shù)據(jù)結(jié)構(gòu)。

2 默認(rèn) Iterator 接口 § ?

Iterator 接口的目的,就是為所有數(shù)據(jù)結(jié)構(gòu),提供了一種統(tǒng)一的訪問機(jī)制,即for...of循環(huán)(詳見下文)。當(dāng)使用for...of循環(huán)遍歷某種數(shù)據(jù)結(jié)構(gòu)時(shí),該循環(huán)會(huì)自動(dòng)去尋找 Iterator 接口。

一種數(shù)據(jù)結(jié)構(gòu)只要部署了 Iterator 接口,我們就稱這種數(shù)據(jù)結(jié)構(gòu)是“可遍歷的”(iterable)。

ES6 規(guī)定,默認(rèn)的 Iterator 接口部署在數(shù)據(jù)結(jié)構(gòu)的Symbol.iterator屬性,或者說,一個(gè)數(shù)據(jù)結(jié)構(gòu)只要具有Symbol.iterator屬性,就可以認(rèn)為是“可遍歷的”(iterable)。Symbol.iterator屬性本身是一個(gè)函數(shù),就是當(dāng)前數(shù)據(jù)結(jié)構(gòu)默認(rèn)的遍歷器生成函數(shù)。執(zhí)行這個(gè)函數(shù),就會(huì)返回一個(gè)遍歷器。至于屬性名Symbol.iterator,它是一個(gè)表達(dá)式,返回Symbol對(duì)象的iterator屬性,這是一個(gè)預(yù)定義好的、類型為 Symbol 的特殊值,所以要放在方括號(hào)內(nèi)(參見《Symbol》一章)。

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

ES6 的有些數(shù)據(jù)結(jié)構(gòu)原生具備 Iterator 接口(比如數(shù)組),即不用任何處理,就可以被for...of循環(huán)遍歷。原因在于,這些數(shù)據(jù)結(jié)構(gòu)原生部署了Symbol.iterator屬性(詳見下文),另外一些數(shù)據(jù)結(jié)構(gòu)沒有(比如對(duì)象)。凡是部署了Symbol.iterator屬性的數(shù)據(jù)結(jié)構(gòu),就稱為部署了遍歷器接口。調(diào)用這個(gè)接口,就會(huì)返回一個(gè)遍歷器對(duì)象。

原生具備 Iterator 接口的數(shù)據(jù)結(jié)構(gòu)如下

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函數(shù)的 arguments 對(duì)象
  • NodeList 對(duì)象
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是一個(gè)數(shù)組,原生就具有遍歷器接口,部署在arr的Symbol.iterator屬性上面。所以,調(diào)用這個(gè)屬性,就得到遍歷器對(duì)象。

對(duì)于原生部署 Iterator 接口的數(shù)據(jù)結(jié)構(gòu),不用自己寫遍歷器生成函數(shù),for...of循環(huán)會(huì)自動(dòng)遍歷它們。除此之外,其他數(shù)據(jù)結(jié)構(gòu)(主要是對(duì)象)的 Iterator 接口,都需要自己在Symbol.iterator屬性上面部署,這樣才會(huì)被for...of循環(huán)遍歷。

對(duì)象(Object)之所以沒有默認(rèn)部署 Iterator 接口,是因?yàn)閷?duì)象的哪個(gè)屬性先遍歷,哪個(gè)屬性后遍歷是不確定的,需要開發(fā)者手動(dòng)指定。本質(zhì)上,遍歷器是一種線性處理,對(duì)于任何非線性的數(shù)據(jù)結(jié)構(gòu),部署遍歷器接口,就等于部署一種線性轉(zhuǎn)換。不過,嚴(yán)格地說,對(duì)象部署遍歷器接口并不是很必要,因?yàn)檫@時(shí)對(duì)象實(shí)際上被當(dāng)作 Map 結(jié)構(gòu)使用,ES5 沒有 Map 結(jié)構(gòu),而 ES6 原生提供了。

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

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
}

上面代碼是一個(gè)類部署 Iterator 接口的寫法。Symbol.iterator屬性對(duì)應(yīng)一個(gè)函數(shù),執(zhí)行后返回當(dāng)前對(duì)象的遍歷器對(duì)象。

下面是通過遍歷器實(shí)現(xiàn)指針結(jié)構(gòu)的例子。

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
}

上面代碼首先在構(gòu)造函數(shù)的原型鏈上部署Symbol.iterator方法,調(diào)用該方法會(huì)返回遍歷器對(duì)象iterator,調(diào)用該對(duì)象的next方法,在返回一個(gè)值的同時(shí),自動(dòng)將內(nèi)部指針移到下一個(gè)實(shí)例。

下面是另一個(gè)為對(duì)象添加 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 };
        }
      }
    };
  }
};

對(duì)于類似數(shù)組的對(duì)象(存在數(shù)值鍵名和length屬性),部署 Iterator 接口,有一個(gè)簡(jiǎn)便方法,就是Symbol.iterator方法直接引用數(shù)組的 Iterator 接口。

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

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

3 調(diào)用 Iterator 接口的場(chǎng)合 § ?

有一些場(chǎng)合會(huì)默認(rèn)調(diào)用 Iterator 接口(即Symbol.iterator方法),除了下文會(huì)介紹的for...of循環(huán),還有幾個(gè)別的場(chǎng)合。

(1)解構(gòu)賦值

對(duì)數(shù)組和 Set 結(jié)構(gòu)進(jìn)行解構(gòu)賦值時(shí),會(huì)默認(rèn)調(diào)用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)擴(kuò)展運(yùn)算符

擴(kuò)展運(yùn)算符(...)也會(huì)調(diào)用默認(rèn)的 Iterator 接口。

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

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

上面代碼的擴(kuò)展運(yùn)算符內(nèi)部就調(diào)用 Iterator 接口。

實(shí)際上,這提供了一種簡(jiǎn)便機(jī)制,可以將任何部署了 Iterator 接口的數(shù)據(jù)結(jié)構(gòu),轉(zhuǎn)為數(shù)組。也就是說,只要某個(gè)數(shù)據(jù)結(jié)構(gòu)部署了 Iterator 接口,就可以對(duì)它使用擴(kuò)展運(yùn)算符,將其轉(zhuǎn)為數(shù)組。
let arr = [...iterable];

(3)yield*

yield*后面跟的是一個(gè)可遍歷的結(jié)構(gòu),它會(huì)調(diào)用該結(jié)構(gòu)的遍歷器接口。


letlet generator  generator ==  functionfunction**  (())  {{
  yield    yield 11;;
  yield   yield**  [[22,,33,,44]];;
  yield    yield 55;;
 }};;

  varvar iterator  iterator ==  generatorgenerator(());;

iterator  iterator..nextnext(()) // { value: 1, done: false }
 // { 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)其他場(chǎng)合

由于數(shù)組的遍歷會(huì)調(diào)用遍歷器接口,所以任何接受數(shù)組作為參數(shù)的場(chǎng)合,其實(shí)都調(diào)用了遍歷器接口。下面是一些例子。

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

4 字符串的 Iterator 接口 § ?

字符串是一個(gè)類似數(shù)組的對(duì)象,也原生具有 Iterator 接口。


varvar someString  someString ==  "hi""hi";;
 typeoftypeof someString someString[[SymbolSymbol..iteratoriterator]]
// "function"
 // "function" 
 varvar iterator  iterator == someString someString[[SymbolSymbol..iteratoriterator]](());;

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

上面代碼中,調(diào)用Symbol.iterator方法返回一個(gè)遍歷器對(duì)象,在這個(gè)遍歷器上可以調(diào)用 next 方法,實(shí)現(xiàn)對(duì)于字符串的遍歷。

可以覆蓋原生的Symbol.iterator方法,達(dá)到修改遍歷器行為的目的。

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方法被修改了,所以擴(kuò)展運(yùn)算符(...)返回的值變成了bye,而字符串本身還是hi。

5 Iterator 接口與 Generator 函數(shù) § ?

Symbol.iterator方法的最簡(jiǎn)單實(shí)現(xiàn),還是使用下一章要介紹的 Generator 函數(shù)。

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

// 或者采用下面的簡(jiǎn)潔寫法

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

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

6 遍歷器對(duì)象的 return(),throw() § ?

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

return方法的使用場(chǎng)合是,如果for...of循環(huán)提前退出(通常是因?yàn)槌鲥e(cuò),或者有break語(yǔ)句),就會(huì)調(diào)用return方法。如果一個(gè)對(duì)象在完成遍歷前,需要清理或釋放資源,就可以部署return方法。

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

上面代碼中,函數(shù)readLinesSync接受一個(gè)文件對(duì)象作為參數(shù),返回一個(gè)遍歷器對(duì)象,其中除了next方法,還部署了return方法。下面的兩種情況,都會(huì)觸發(fā)執(zhí)行return方法。

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

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

上面代碼中,情況一輸出文件的第一行以后,就會(huì)執(zhí)行return方法,關(guān)閉這個(gè)文件;情況二會(huì)在執(zhí)行return方法關(guān)閉文件之后,再拋出錯(cuò)誤。

注意,return方法必須返回一個(gè)對(duì)象,這是 Generator 規(guī)格決定的。

throw方法主要是配合 Generator 函數(shù)使用,一般的遍歷器對(duì)象用不到這個(gè)方法。請(qǐng)參閱《Generator 函數(shù)》一章。

7 for...of 循環(huán) § ?

ES6 借鑒 C++、Java、C# 和 Python 語(yǔ)言,引入了for...of循環(huán),作為遍歷所有數(shù)據(jù)結(jié)構(gòu)的統(tǒng)一的方法。

一個(gè)數(shù)據(jù)結(jié)構(gòu)只要部署了Symbol.iterator屬性,就被視為具有 iterator 接口,就可以用for...of循環(huán)遍歷它的成員。也就是說,for...of循環(huán)內(nèi)部調(diào)用的是數(shù)據(jù)結(jié)構(gòu)的Symbol.iterator方法。

for...of循環(huán)可以使用的范圍包括數(shù)組、Set 和 Map 結(jié)構(gòu)、某些類似數(shù)組的對(duì)象(比如arguments對(duì)象、DOM NodeList 對(duì)象)、后文的 Generator 對(duì)象,以及字符串。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容