ES6之Iterator和for...of

Iterator

一、概念

遍歷器(Iterator)主要就是為了向JavaScript中的數(shù)組(Array)和對象(Object),以及ES6 新加的Map和Set提供一種統(tǒng)一的訪問接口機(jī)制,來處理不同的數(shù)據(jù)結(jié)構(gòu),只要數(shù)據(jù)結(jié)構(gòu)中實現(xiàn)了Iterator,就可以依次處理其中的所有成員。

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

二、遍歷過程

1.創(chuàng)建一個指針對象,指向當(dāng)前數(shù)據(jù)結(jié)構(gòu)的起始位置
2.首次調(diào)用指針對象的next方法,指針將指向數(shù)據(jù)結(jié)構(gòu)的第一個成員,其后依次調(diào)用,依次指向后面的成員
3.不斷調(diào)用指針對象的next方法,直到它指向數(shù)據(jù)結(jié)構(gòu)的結(jié)束位置為止

每一次調(diào)用next方法,都會返回數(shù)據(jù)結(jié)構(gòu)的當(dāng)前成員的信息。具體內(nèi)容是一個包含valuedone兩個屬性的對象。其中,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};
    }
  };
}

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

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

由于 Iterator 只是把接口規(guī)格加到數(shù)據(jù)結(jié)構(gòu)之上,所以,遍歷器與它所遍歷的那個數(shù)據(jù)結(jié)構(gòu),實際上是分開的,完全可以寫出沒有對應(yīng)數(shù)據(jù)結(jié)構(gòu)的遍歷器對象,或者說用遍歷器對象模擬出數(shù)據(jù)結(jié)構(gòu)。下面是一個無限運(yùn)行的遍歷器對象的例子:

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

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

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

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

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

三、默認(rèn) 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屬性,或者說,一個數(shù)據(jù)結(jié)構(gòu)只要具有Symbol.iterator屬性,就可以認(rèn)為是“可遍歷的”(iterable)。
  • Symbol.iterator屬性本身是一個函數(shù),就是當(dāng)前數(shù)據(jù)結(jié)構(gòu)默認(rèn)的遍歷器生成函數(shù)。執(zhí)行這個函數(shù),就會返回一個遍歷器。至于屬性名Symbol.iterator,它是一個表達(dá)式,返回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方法,都會返回一個具有valuedone屬性的對象。

ES6 的有些原生數(shù)據(jù)結(jié)構(gòu)具備Iterator接口(比如數(shù)組),即不用任何處理,就可以被 for...of循環(huán)遍歷。因為這些數(shù)據(jù)結(jié)構(gòu)原生部署了Symbol.iterator屬性,但是另外一些數(shù)據(jù)結(jié)構(gòu)沒有(比如對象)。

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

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 函數(shù)的 arguments 對象
  • NodeList 對象

下面的例子是數(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 }

一個對象如果要具備可被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)前對象的遍歷器對象。

下面是通過遍歷器實現(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)用該方法會返回遍歷器對象iterator,調(diào)用該對象的next方法,在返回一個值的同時,自動將內(nèi)部指針移到下一個實例。

四、調(diào)用 Iterator 接口的場合

  • 除了一些默認(rèn)數(shù)據(jù)結(jié)構(gòu)會調(diào)用 Iterator 接口(即Symbol.iterator方法),還有幾個特別的場合:

1.結(jié)構(gòu)賦值
對數(shù)組和 Set 結(jié)構(gòu)進(jìn)行解構(gòu)賦值時,會默認(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)算符(...)也會調(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 接口,實際上,這提供了一種簡便機(jī)制,可以將任何部署了 Iterator 接口的數(shù)據(jù)結(jié)構(gòu),轉(zhuǎn)為數(shù)組。也就是說,只要某個數(shù)據(jù)結(jié)構(gòu)部署了 Iterator 接口,就可以對它使用擴(kuò)展運(yùn)算符,將其轉(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 }

也可以覆蓋原生的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"

六、Symbol.iterator方法的簡單實現(xiàn)

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

let 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()方法

要實現(xiàn)遍歷器對象生成函數(shù),那么next方法是必須部署的,return方法和throw方法是可選的。

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

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

上面代碼中,函數(shù)readLinesSync接受一個文件對象作為參數(shù),返回一個遍歷器對象,其中除了next方法,還部署了return方法。下面的兩種情況,都會觸發(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();
}

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

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

throw方法主要是配合 Generator 函數(shù)使用,一般的遍歷器對象用不到這個方法。

八、默認(rèn)部署了Symbol.iterator屬性的幾種數(shù)據(jù)結(jié)構(gòu)

1.數(shù)組

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部署了數(shù)組arrSymbol.iterator屬性,結(jié)果objfor...of循環(huán),產(chǎn)生了與arr完全一樣的結(jié)果。所以for...of循環(huán)本質(zhì)上就是調(diào)用iterator這個接口產(chǎn)生的遍歷器。

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方法。

for...of循環(huán)調(diào)用遍歷器接口,數(shù)組的遍歷器接口只返回具有數(shù)字索引的屬性。這一點(diǎn)跟for...in循環(huán)也不一樣。

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循環(huán)不會返回數(shù)組arrfoo屬性。

2.SetMap 結(jié)構(gòu)
Set 和 Map 結(jié)構(gòu)也原生具有 Iterator 接口,可以直接使用for...of循環(huán)。

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

for...of只能返回遍歷器中的值,但是如果想用來獲取索引,可以使用entrieskeys來實現(xiàn):

entries() 返回一個遍歷器對象,用來遍歷[鍵名, 鍵值]組成的數(shù)組。對于數(shù)組,鍵名就是索引值;對于 Set,鍵名與鍵值相同。Map 結(jié)構(gòu)的 Iterator 接口,默認(rèn)就是調(diào)用entries方法。
keys() 返回一個遍歷器對象,用來遍歷所有的鍵名。
values() 返回一個遍歷器對象,用來遍歷所有的鍵值。

let arr = ['a', 'b', 'c'];
for (let pair of arr.entries()) {
  console.log(pair);
}
// [0, 'a']
// [1, 'b']
// [2, 'c']

3.類似數(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)還有一個特點(diǎ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);
}

4.對象
對于普通的對象,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

一種解決方法是,使用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

九、與其他遍歷語法的比較

  • for循環(huán)
for (var index = 0; index < myArray.length; index++) {
  console.log(myArray[index]);
}
  • forEach方法
myArray.forEach(function (value) {
  console.log(value);
});

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

  • for...in循環(huán)

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

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

for...in循環(huán)有幾個缺點(diǎ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

for (let value of myArray) {
  console.log(value);
}
  • 有著同for...in一樣的簡潔語法,但是沒有for...in那些缺點(diǎn)。
  • 不同于forEach方法,它可以與breakcontinuereturn配合使用。
  • 提供了遍歷所有數(shù)據(jù)結(jié)構(gòu)的統(tǒng)一操作接口。

下面是一個使用 break 語句,跳出for...of循環(huán)的例子。

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

參考

http://es6.ruanyifeng.com/#docs/iterator

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

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

  • Iterator(遍歷器)的概念 JavaScript原有的表示“集合”的數(shù)據(jù)結(jié)構(gòu),主要是數(shù)組和對象,ES6又添加...
    oWSQo閱讀 661評論 0 1
  • Iterator(遍歷器)的概念 JavaScript原有的表示“集合”的數(shù)據(jù)結(jié)構(gòu),主要是數(shù)組(Array)和對象...
    呼呼哥閱讀 4,540評論 0 2
  • 一、Iterator(遍歷器)的概念 Iterator 是一種接口,為各種不同的數(shù)據(jù)結(jié)構(gòu)提供統(tǒng)一的訪問機(jī)制。 任何...
    magic_pill閱讀 354評論 0 1
  • 我曾經(jīng)神在大唐 整人卻在蘇杭 世間完美的一切 統(tǒng)統(tǒng)皆入我囊 可是不曾想過 愛上了你 像一對神仙眷侶 同你一起開啟 ...
    亦沐北風(fēng)閱讀 401評論 3 6
  • 我的青春,一直在和房子較勁。 我家住在縣城中心,以前叫做城壕的一處小巷子里,不深,大約一百來米,但地勢低,從街頭到...
    靈動語文閱讀 1,059評論 3 16

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