1
0
mirror of https://github.com/ruanyf/es6tutorial.git synced 2025-05-24 18:32:22 +00:00
es6tutorial/docs/iterator.md
2015-01-27 21:52:28 +08:00

9.5 KiB
Raw Blame History

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方法返回值的结构value和done两个属性。

Iterator接口的部署

具有Iterator接口的对象都能被for...of循环遍历见后文的介绍。所谓Iterator接口就是指它会返回一个遍历器对象该对象具备next方法每次调用该方法会依次返回一个具有上节提到的value和done两个属性的新对象指向原对象的一个成员。

在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,键值是一个方法(函数),该方法执行后,返回一个当前对象的遍历器。

下面是一个例子。


function O(value){
  this.value = value;
  this.next = null;
}

O.prototype[Symbol.iterator] = function(){
  
  var iterator = {
    next: next
  };
  
  var current = this;

  function next(){
    if (current){
      var value = current.value;
      var done = current == null;
      current = current.next;
      return {
        done: done,
        value: value
      }
    } else {
      return {
        done: true
      }
    }
  }
  return iterator;
}

var one = new O(1);
var two = new O(2);
var three = new O(3);
one.next = two;
two.next = three;

for (var i of one){
  console.log(i)
}
// 1
// 2
// 3

上面代码首先在构造函数的原型链上部署Symbol.iterator方法调用该方法会返回遍历器对象iterator调用该对象的next方法在返回一个值的同时自动将内部指针移到下一个实例。

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

如果Symbol.iterator方法返回的不是遍历器,解释引擎将会报错。


var obj = {};

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

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

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

原生具备iterator接口的数据结构

《数组的扩展》一章中提到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。

Iterator接口与Generator函数

部署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

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