Iterator和for...of循环

Iterator(遍历器)

遍历器(Iterator)是一种接口规格,任何对象只要部署这个接口,就可以完成遍历操作。它的作用有两个,一是为各种数据结构,提供一个统一的、简便的接口,二是使得对象的属性能够按某种次序排列。在ES6中,遍历操作特指for...of循环,即Iterator接口主要供for...of循环使用。

遍历器提供了一个指针,指向当前对象的某个属性,使用next方法,就可以将指针移动到下一个属性。next方法返回一个包含value和done两个属性的对象。其中,value属性是当前遍历位置的值,done属性是一个布尔值,表示遍历是否结束。下面是一个模拟next方法返回值的例子。


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

var it = makeIterator(['a', 'b']);

it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }

上面代码定义了一个makeIterator函数,它的作用是返回一个遍历器对象,用来遍历参数数组。next方法依次遍历数组的每个成员,请特别注意,next返回值的构造。

下面是一个无限运行的遍历器例子。


function idMaker(){
  var index = 0;

  return {
    next: function(){
      return {value: index++, done: false};
    }
  }
}

var it = idMaker();

it.next().value // '0'
it.next().value // '1'
it.next().value // '2'
// ...

上面的例子,只是为了说明next方法返回值的结构。Iterator接口返回的遍历器,原生具备next方法,不用自己部署。所以,真正需要部署的是Iterator接口,让其返回一个遍历器。在ES6中,有三类数据结构原生具备Iterator接口:数组、类似数组的对象、Set和Map结构。除此之外,其他数据结构(主要是对象)的Iterator接口都需要自己部署。

下面就是如何部署Iterator接口。一个对象如果要有Iterator接口,必须部署一个@@iterator方法(原型链上的对象具有该方法也可),该方法部署在一个键名为Symbol.iterator的属性上,对应的键值是一个函数,该函数返回一个遍历器对象。


class MySpecialTree {
  // ...
  [Symbol.iterator]() { 
    // ...
    return theIterator;
  }
}

上面代码是一个类部署Iterator接口的写法。Symbol.iterator是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为Symbol的特殊值,所以要放在方括号内(请参考Symbol一节)。这里要注意,@@iterator的键名是Symbol.iterator,键值是一个方法(函数),该方法执行后,返回一个当前对象的遍历器。

下面是为对象添加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 };
        }
      }
    };
  }
};

《数组的扩展》一章中提到,ES6对数组提供entries()、keys()和values()三个方法,就是返回三个遍历器。


var arr = [1, 5, 7];
var arrEntries = arr.entries();

arrEntries.toString() 
// "[object Array Iterator]"

arrEntries === arrEntries[Symbol.iterator]()
// true

上面代码中,entries方法返回的是一个遍历器(iterator),本质上就是调用了Symbol.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 }

上面代码中,调用Symbol.iterator方法返回一个遍历器,在这个遍历器上可以调用next方法,实现对于字符串的遍历。

可以覆盖原生的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。

部署Symbol.iterator方法的最简单实现,还是使用下一节要提到的Generator函数。


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方法返回的不是遍历器,解释引擎将会报错。


var obj = {};

obj[Symbol.iterator] = () => 1;

[...obj] // TypeError: [] is not a function

上面代码中,变量obj的@@iterator方法返回的不是遍历器,因此报错。

for...of循环

ES6中,一个对象只要部署了@@iterator方法,就被视为具有iterator接口,就可以用for...of循环遍历它的值。也就是说,for...of循环内部调用是原对象的Symbol.iterator方法。

数组原生具备iterator接口。


const arr = ['red', 'green', 'blue'];

for(let v of arr) {
    console.log(v); // red green blue
}

上面代码说明,for...of循环可以代替数组实例的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循环,只能获得对象的键名,不能直接获取键值。ES6提供for...of循环,允许遍历获得键值。


var arr = ["a", "b", "c", "d"];
for (a in arr) {
  console.log(a); // 0 1 2 3
}

for (a of arr) {
  console.log(a); // a b c d
}

上面代码表明,for...in循环读取键名,for...of循环读取键值。如果要通过for...of循环,获取数组的索引,可以借助数组实例的entries方法和keys方法,参见《数组的扩展》章节。

Set和Map结构也原生具有Iterator接口,可以直接使用for...of循环。


var engines = 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结构和Map结构,后者是同时遍历键名和键值。

对于普通的对象,for...of结构不能直接使用,会报错,必须部署了iterator接口后才能使用。但是,这样情况下,for...in循环依然可以用来遍历键名。


var es6 = {
  edition: 6,
  committee: "TC39",
  standard: "ECMA-262"
};

for (e in es6) {
  console.log(e);
}
// edition
// committee
// standard

for (e of es6) {
  console.log(e);
}
// TypeError: es6 is not iterable

上面代码表示,for...in循环可以遍历键名,for...of循环会报错。

总结一下,for...of循环可以使用的范围包括数组、类似数组的对象(比如arguments对象、DOM NodeList对象)、Set和Map结构、后文的Generator对象,以及字符串。下面是for...of循环用于字符串和DOM NodeList对象的例子。


// 字符串的例子

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