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

Iterator(遍歷器)的概念

JavaScript原有的表示“集合”的數(shù)據(jù)結(jié)構(gòu)退个,主要是數(shù)組和對象执赡,ES6又添加了MapSet姚垃。這樣就有了四種數(shù)據(jù)集合,用戶還可以組合使用它們坏怪,定義自己的數(shù)據(jù)結(jié)構(gòu)贝润,比如數(shù)組的成員是MapMap的成員是對象陕悬。這樣就需要一種統(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)建一個指針對象粪牲,指向當前數(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)的當前成員的信息购公。具體來說赵哲,就是返回一個包含valuedone兩個屬性的對象。其中君丁,value屬性是當前成員的值枫夺,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方法吧趣,用來移動指針。開始時耙厚,指針指向數(shù)組的開始位置强挫。然后,每次調(diào)用next方法薛躬,指針就會指向數(shù)組的下一個成員俯渤。第一次調(diào)用,指向a型宝;第二次調(diào)用八匠,指向b
next方法返回一個對象趴酣,表示當前數(shù)據(jù)成員的信息梨树。這個對象具有valuedone兩個屬性,value屬性返回當前位置的成員岖寞,done屬性是一個布爾值抡四,表示遍歷是否結(jié)束,即是否還有必要再一次調(diào)用next方法慎璧。
總之床嫌,調(diào)用指針對象的next方法,就可以遍歷事先給定的數(shù)據(jù)結(jié)構(gòu)胸私。
對于遍歷器對象來說厌处,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),實際上是分開的瑰排,完全可以寫出沒有對應數(shù)據(jù)結(jié)構(gòu)的遍歷器對象贯要,或者說用遍歷器對象模擬出數(shù)據(jù)結(jié)構(gòu)。下面是一個無限運行的遍歷器對象的例子椭住。

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

上面的例子中崇渗,遍歷器生成函數(shù)idMaker,返回一個遍歷器對象(即指針對象)京郑。但是并沒有對應的數(shù)據(jù)結(jié)構(gòu)宅广,或者說,遍歷器對象自己描述了一個數(shù)據(jù)結(jié)構(gòu)出來些举。
如果使用TypeScript的寫法跟狱,遍歷器接口(Iterable)、指針對象(Iterator)和next方法返回值的規(guī)格可以描述如下户魏。

interface Iterable {
  [Symbol.iterator]() : Iterator,
}
interface Iterator {
  next(value?: any) : IterationResult,
}
interface IterationResult {
  value: any,
  done: boolean,
}

默認Iterator接口

Iterator接口的目的驶臊,就是為所有數(shù)據(jù)結(jié)構(gòu),提供了一種統(tǒng)一的訪問機制叼丑,即for...of循環(huán)关翎。當使用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ù)沮明,就是當前數(shù)據(jù)結(jié)構(gòu)默認的遍歷器生成函數(shù)辕坝。執(zhí)行這個函數(shù),就會返回一個遍歷器荐健。至于屬性名Symbol.iterator酱畅,它是一個表達式,返回Symbol對象的iterator屬性江场,這是一個預定義好的纺酸、類型為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)沒有(比如對象)。凡是部署了Symbol.iterator屬性的數(shù)據(jù)結(jié)構(gòu)芽唇,就稱為部署了遍歷器接口顾画。調(diào)用這個接口,就會返回一個遍歷器對象匆笤。
原生具備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 }

上面代碼中,變量arr是一個數(shù)組书蚪,原生就具有遍歷器接口喇澡,部署在arrSymbol.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)遍歷。
對象之所以沒有默認部署Iterator接口琅催,是因為對象的哪個屬性先遍歷居凶,哪個屬性后遍歷是不確定的,需要開發(fā)者手動指定藤抡。本質(zhì)上侠碧,遍歷器是一種線性處理,對于任何非線性的數(shù)據(jù)結(jié)構(gòu)缠黍,部署遍歷器接口弄兜,就等于部署一種線性轉(zhuǎn)換。不過瓷式,嚴格地說替饿,對象部署遍歷器接口并不是很必要,因為這時對象實際上被當作Map結(jié)構(gòu)使用贸典,ES5沒有Map結(jié)構(gòu)视卢,而ES6原生提供了。
一個對象如果要具備可被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屬性對應一個函數(shù)妒挎,執(zhí)行后返回當前對象的遍歷器對象绳锅。
下面是通過遍歷器實現(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)部指針移到下一個實例原朝。
下面是另一個為對象添加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 };
        }
      }
    };
  }
};

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

NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
// 或者
NodeList.prototype[Symbol.iterator] = [][Symbol.iterator];
[...document.querySelectorAll('div')] // 可以執(zhí)行了

NodeList對象是類似數(shù)組的對象,本來就具有遍歷接口谢澈,可以直接遍歷煌贴。上面代碼中,我們將它的遍歷接口改成數(shù)組的Symbol.iterator屬性锥忿,可以看到?jīng)]有任何影響牛郑。
下面是另一個類似數(shù)組的對象調(diào)用數(shù)組的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'
}

注意敬鬓,普通對象部署數(shù)組的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方法對應的不是遍歷器生成函數(shù)(即會返回一個遍歷器對象),解釋引擎將會報錯础芍。

var obj = {};
obj[Symbol.iterator] = () => 1;
[...obj] // TypeError: [] is not a function

上面代碼中杈抢,變量objSymbol.iterator方法對應的不是遍歷器生成函數(shù),因此報錯仑性。
有了遍歷器接口惶楼,數(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)朦乏,還有幾個別的場合。

解構(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'];

擴展運算符

擴展運算符(...)也會調(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];

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 }

其他場合

由于數(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"

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

Iterator接口與Generator函數(shù)

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

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"

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

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

遍歷器對象除了具有next方法,還可以具有return方法和throw方法魄咕。如果你自己寫遍歷器對象生成函數(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方法虱饿,關閉這個文件;情況二會在執(zhí)行return方法關閉文件之后渴肉,再拋出錯誤爽冕。
注意颈畸,return方法必須返回一個對象,這是Generator規(guī)格決定的礁苗。
throw方法主要是配合Generator函數(shù)使用试伙,一般的遍歷器對象用不到這個方法。

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

ES6引入了for...of循環(huán)吱抚,作為遍歷所有數(shù)據(jù)結(jié)構(gòu)的統(tǒng)一的方法。
一個數(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ù)組壹甥、SetMap結(jié)構(gòu)句柠、某些類似數(shù)組的對象(比如arguments對象、DOM NodeList對象)精盅、Generator對象叹俏,以及字符串甚带。

數(shù)組

數(shù)組原生具備iterator接口(即默認部署了Symbol.iterator屬性)鹰贵,for...of循環(huán)本質(zhì)上就是調(diào)用這個接口產(chǎn)生的遍歷器碉输。

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)可以代替數(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ù)字索引的屬性。這一點跟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屬性罐旗。

Set和Map結(jié)構(gòu)

SetMap結(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

上面代碼演示了如何遍歷Set結(jié)構(gòu)和Map結(jié)構(gòu)涂滴。值得注意的地方有兩個柔纵,首先,遍歷的順序是按照各個成員被添加進數(shù)據(jù)結(jié)構(gòu)的順序或详。其次霸琴,Set結(jié)構(gòu)遍歷時昭伸,返回的是一個值庐杨,而Map結(jié)構(gòu)遍歷時灵份,返回的是一個數(shù)組,該數(shù)組的兩個成員分別為當前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

計算生成的數(shù)據(jù)結(jié)構(gòu)

有些數(shù)據(jù)結(jié)構(gòu)是在現(xiàn)有數(shù)據(jù)結(jié)構(gòu)的基礎上横浑,計算生成的。比如洒缀,ES6的數(shù)組树绩、Set饺饭、Map都部署了以下三個方法职车,調(diào)用后都返回遍歷器對象悴灵。

  • entries()返回一個遍歷器對象,用來遍歷[鍵名, 鍵值]組成的數(shù)組川尖。對于數(shù)組叮喳,鍵名就是索引值嘲更;對于Set揩瞪,鍵名與鍵值相同李破。Map結(jié)構(gòu)的Iterator接口,默認就是調(diào)用entries方法毛嫉。
  • keys()返回一個遍歷器對象承粤,用來遍歷所有的鍵名辛臊。
  • values()返回一個遍歷器對象,用來遍歷所有的鍵值伐割。

這三個方法調(diào)用后生成的遍歷器對象隔心,所遍歷的都是計算生成的數(shù)據(jù)結(jié)構(gòu)尚胞。

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

類似數(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ù)組亥贸。
for...of循環(huán)相比上面幾種做法,有一些顯著的優(yōu)點浇垦。

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

有著同for...in一樣的簡潔語法炕置,但是沒有for...in那些缺點。
不同于forEach方法男韧,它可以與break朴摊、continuereturn配合使用。
提供了遍歷所有數(shù)據(jù)結(jié)構(gòu)的統(tǒng)一操作接口此虑。

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

上面的例子甚纲,會輸出斐波納契數(shù)列小于等于1000的項朦前。

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末介杆,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子韭寸,更是在濱河造成了極大的恐慌春哨,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件恩伺,死亡現(xiàn)場離奇詭異赴背,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門癞尚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來耸三,“玉大人,你說我怎么就攤上這事浇揩∫亲常” “怎么了?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵胳徽,是天一觀的道長积锅。 經(jīng)常有香客問我,道長养盗,這世上最難降的妖魔是什么缚陷? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮往核,結(jié)果婚禮上箫爷,老公的妹妹穿的比我還像新娘。我一直安慰自己聂儒,他們只是感情好虎锚,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著衩婚,像睡著了一般窜护。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上非春,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天柱徙,我揣著相機與錄音,去河邊找鬼奇昙。 笑死护侮,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的储耐。 我是一名探鬼主播羊初,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼弧岳!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起业踏,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤禽炬,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后勤家,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體腹尖,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年伐脖,在試婚紗的時候發(fā)現(xiàn)自己被綠了热幔。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片乐设。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖绎巨,靈堂內(nèi)的尸體忽然破棺而出近尚,到底是詐尸還是另有隱情,我是刑警寧澤场勤,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布戈锻,位于F島的核電站,受9級特大地震影響和媳,放射性物質(zhì)發(fā)生泄漏格遭。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一留瞳、第九天 我趴在偏房一處隱蔽的房頂上張望拒迅。 院中可真熱鬧,春花似錦她倘、人聲如沸璧微。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽往毡。三九已至,卻和暖如春靶溜,著一層夾襖步出監(jiān)牢的瞬間开瞭,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工罩息, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留嗤详,地道東北人。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓瓷炮,卻偏偏與公主長得像葱色,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子娘香,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345

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