Iterator 與for...of...

原生具備 Iterator 接口的數(shù)據(jù)結(jié)構(gòu)如下。
Array
Map
Set
String
TypedArray
函數(shù)的 arguments 對象
NodeList 對象

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

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

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

Iterator 的遍歷過程是這樣的。

(1)創(chuàng)建一個指針對象,指向當(dāng)前數(shù)據(jù)結(jié)構(gòu)的起始位置。也就是說,遍歷器對象本質(zhì)上,就是一個指針對象。
(2)第一次調(diào)用指針對象的next方法,可以將指針指向數(shù)據(jù)結(jié)構(gòu)的第一個成員。
(3)第二次調(diào)用指針對象的next方法,指針就指向數(shù)據(jù)結(jié)構(gòu)的第二個成員。
(4)不斷調(diào)用指針對象的next方法,直到它指向數(shù)據(jù)結(jié)構(gòu)的結(jié)束位置。

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

下面是一個模擬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函數(shù),它是一個遍歷器生成函數(shù),作用就是返回一個遍歷器對象。對數(shù)組['a', 'b']執(zhí)行這個函數(shù),就會返回該數(shù)組的遍歷器對象(即指針對象)it。

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

默認 Iterator 接口

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

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

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

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

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

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

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

一個對象如果要具備可被for...of循環(huán)調(diào)用的 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屬性對應(yīng)一個函數(shù),執(zhí)行后返回當(dāng)前對象的遍歷器對象。
有了遍歷器接口,數(shù)據(jù)結(jié)構(gòu)就可以用for...of循環(huán)遍歷(詳見下文),也可以使用while循環(huán)遍歷。

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

上面代碼中,ITERABLE代表某種可遍歷的數(shù)據(jù)結(jié)構(gòu),$iterator是它的遍歷器對象。遍歷器對象每次移動指針(next方法),都檢查一下返回值的done屬性,如果遍歷還沒結(jié)束,就移動遍歷器對象的指針到下一步(next方法),不斷循環(huán)。
調(diào)用 Iterator 接口的場合
有一些場合會默認調(diào)用 Iterator 接口(即Symbol.iterator方法),除了下文會介紹的for...of循環(huán),還有幾個別的場合。

調(diào)用iterator的場合

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

對數(shù)組和 Set 結(jié)構(gòu)進行解構(gòu)賦值時,會默認調(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)擴展運算符

擴展運算符(...)也會調(diào)用默認的 Iterator 接口。

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

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

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

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

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

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

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)其他場合

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

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

字符串的 Iterator 接口

字符串是一個類似數(shù)組的對象,也原生具有 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 }

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

可以覆蓋原生的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函數(shù)

Symbol.iterator方法的最簡單實現(xiàn),還是使用Generator函數(shù)。

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命令給出每一步的返回值即可。
for...of循環(huán)可以代替數(shù)組實例的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循環(huán),只能獲得對象的鍵名,不能直接獲取鍵值。ES6 提供for...of循環(huán),允許遍歷獲得鍵值。

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循環(huán)讀取鍵名,for...of循環(huán)讀取鍵值。如果要通過for...of循環(huán),獲取數(shù)組的索引,可以借助數(shù)組實例的entries方法和keys方法.

類似數(shù)組的對象

類似數(shù)組的對象包括好幾類。下面是for...of
循環(huán)用于字符串、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
循環(huán)還有一個特點,就是會正確識別32位 UTF-16 字符。
for (let x of 'a\uD83D\uDC0A') { console.log(x);}// 'a'// '\uD83D\uDC0A'

并不是所有類似數(shù)組的對象都具有 Iterator 接口,一個簡便的解決方法,就是使用Array.from
方法將其轉(zhuǎn)為數(shù)組。

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結(jié)構(gòu)不能直接使用,會報錯,必須部署了 Iterator 接口后才能使用。但是,這樣情況下,for...in循環(huán)依然可以用來遍歷鍵名。

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循環(huán)可以遍歷鍵名,for...of循環(huán)會報錯。

一種解決方法是,使用Object.keys方法將對象的鍵名生成一個數(shù)組,然后遍歷這個數(shù)組。

for (var key of Object.keys(someObject)) {
console.log(key + ': ' + someObject[key]);
}

另一個方法是使用 Generator 函數(shù)將對象重新包裝一下。

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

##與其他遍歷語法的比較
以數(shù)組為例,JavaScript 提供多種遍歷語法。最原始的寫法就是for循環(huán)。

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

這種寫法比較麻煩,因此數(shù)組提供內(nèi)置的forEach方法。

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

這種寫法的問題在于,*無法中途跳出forEach循環(huán)*,break命令或return命令都不能奏效。

for...in循環(huán)可以遍歷數(shù)組的鍵名。

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

for...in循環(huán)有幾個缺點。

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

for...of循環(huán)相比上面幾種做法,有一些顯著的優(yōu)點。

for (let value of myArray) {
console.log(value);
}

有著同for...in一樣的簡潔語法,但是沒有for...in那些缺點。
*不同于forEach方法,它可以與break、continue和return配合使用。*
提供了遍歷所有數(shù)據(jù)結(jié)構(gòu)的統(tǒng)一操作接口。
下面是一個使用break語句,跳出for...of循環(huán)的例子。

for (var n of fibonacci) {
if (n > 1000)
break;
console.log(n);
}

上面的例子,會輸出斐波納契數(shù)列小于等于1000的項。如果當(dāng)前項大于1000,就會使用break語句跳出for...of循環(huán)。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,908評論 6 541
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,324評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,018評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,675評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,417評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,783評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,779評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,960評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,522評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,267評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,471評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,009評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,698評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,099評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,386評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,204評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,436評論 2 378

推薦閱讀更多精彩內(nèi)容

  • Iterator(遍歷器)的概念 JavaScript原有的表示“集合”的數(shù)據(jù)結(jié)構(gòu),主要是數(shù)組(Array)和對象...
    呼呼哥閱讀 4,469評論 0 2
  • for in for in可以用來遍歷對象的可枚舉屬性列表(包括[[prototype]]鏈),單獨使用in操作符...
    ascend13閱讀 867評論 0 1
  • 一、let 和 constlet:變量聲明, const:只讀常量聲明(聲明的時候賦值)。 let 與 var 的...
    dadage456閱讀 768評論 0 0
  • 昨天我華小兔首次更新一張有對話的漫畫,大家應(yīng)該看到了。 畫風(fēng)是這樣的: “你有看到我朋友嗎?” “他長什么樣?” ...
    華小兔漫畫閱讀 270評論 0 0
  • 我的學(xué)習(xí)筆記的參考來源來自唐巧的博客,今天早上開始學(xué)習(xí)他在基礎(chǔ)篇所講的排版引擎框架,首先我看到了這個"單一功能原則...
    iOSDevVicky閱讀 513評論 0 2