迭代器 Iterator

迭代器对象有一个 next 方法,每次执行后返回一个结果对象。结果对象有两个属性,一个是 value ,另一个是 done 。

迭代器对象还有一个内部属性,用于指向集合中的当前位置。类似于 for 循环的变量 i 。

迭代器通常会保存集合的引用。这意味着集合的元素发生改变时,会影响迭代器的返回值。

如下代码所示:

let arr = [1, 3, 5];
let iterator = (function() {
  let i = 0;
  return {
    next: function() {
      let done = i >= arr.length;
      let value = done ? undefined : arr[i++];
      return {
        done: done,
        value: value
      };
    }
  }
})()

使用立即执行函数表达式(IIFE),将返回值赋给 iterator ,此时 iterator 为迭代器对象。 同时变量 i 只有这个迭代器对象的 next 方法可以访问到,并且每次执行都会将 i 的值递增,这意味着迭代器对象只能迭代一遍。

另外,因为迭代器对象保存的是 arr 的引用,所以当 arr 内的元素改变时,也会影响迭代器的 next 方法的返回值。

以上代码是 ECMAScript 6 之前创建迭代器的方式,在 ECMAScript 6 标准里,使用 生成器函数 Generator Function 更方便创建迭代器。

生成器 Generator

生成器函数通常配合新的关键字 yield 一起使用。如下代码所示:

let arr = [1, 3, 5];
let iterator = (function *(arr) {
  for (let i = 0, len = arr.length; i < len; i++) {
    yield arr[i];
  }
})(arr)

生成器函数创建的 iterator 对象也有 next 方法,且行为和之前迭代器部分的示例代码类似。

但两种方式创建得到的迭代器有一些不同,原来的方法得到的 iterator 只是一个拥有 next 方法的普通对象,是 Object 的实例。 而生成器函数创建的对象是 Generator 的一个实例,且除了 next 方法,还有 return 和 throw 方法。

生成器函数和普通函数有一些不同,不只是定义函数时多写一个星号,在行为上也有不同。

在普通函数中使用 return 关键字会提前从被调函数返回到主调函数,也会影响函数的返回值。 而在生成器函数中,使用 return 关键字只会提前返回,但无法改变函数的返回值,生成器函数的返回值始终是一个迭代器对象。 而 return 的值会作为这个迭代器对象调用 next 方法的最终结果值。

另外,新的 yield 关键字只能在生成器函数中使用,在普通函数中使用会报错,即使是嵌套关系也不行。 如下代码所示:

(function *() {
  (function () {
    yield 'value';
  })()
})()

与生成器和迭代器相关的,还有一种 可迭代对象 Iterable 。

可迭代对象 Iterable

可迭代对象有一个属性,值为函数,而键名是 Symbol.iterator 这个唯一值。

新的 for-of 循环和展开运算符都需要接收一个 可迭代对象 。 如果处理的是一个对象,则会判断对象上是否有 Symbol.iterator 这个属性,属性值是否是一个函数,是则执行这个函数,得到迭代器。 否则抛出一个错误,提示该对象不是一个 可迭代对象 。

ES6 中数组和 Map、Set 的实例都默认有 Symbol.iterator 属性,所以可以用于 for-of 循环和展开运算符。

要自定义一个 可迭代对象 ,可以在一个对象上增加 Symbol.iterator 属性,代码如下:

let arr = [1, 3, 5];
let iterator = (function() {
  let i = 0;
  return {
    next: function() {
      let done = i >= arr.length;
      let value = done ? undefined : arr[i++];
      return {
        done: done,
        value: value
      };
    }
  }
})()
iterator[Symbol.iterator] = function() {
  return this;
}

iterator 既是一个 迭代器对象,同是又是一个 可迭代对象 。 因为它既有 next 方法,又有 Symbol.iterator 属性。

这个 iterator 对象也可以用于 for-of 循环和展开运算符,如下所示:

let arr_new = [...iterator];  // arr_new = [1, 3, 5]

这意味着, 可迭代对象 的 Symbol.iterator 属性值不一定非得是生成器函数,即使是普通函数,但只要函数返回值是迭代器即可。

另外,Symbol.iterator 属性所对应的函数里可以使用 this 。 在 for-of 循环和展开运算符调用的情况下,this 会指向 可迭代对象 。

而且,Symbol.iterator 属性所对应函数的返回值不一定非得是一个 Generator 的实例。 即使是一个普通对象,但有 next 方法即可。

这里的 iterator 作为 可迭代对象 有一个缺点,因为它的 Symbol.iterator 属性对应的函数多次调用都会返回 iterator 自身 ,但 iterator 的内部属性 i 在经历了一遍迭代后,就不会再变回零值,所以 iterator 只能用一次。如下代码所示:

let arr_new = [...iterator];      // arr_new = [1, 3, 5]
let arr_latest = [...iterator];   // arr_latest = []

数组和 Map 、 Set 的实例都有 values keys 、 entries 方法,这些方法的返回值都是 可迭代对象 ,同是又是 迭代器对象 。 因为这些方法所返回的对象上,Symbol.iterator 属性对应的函数执行后,返回值是迭代器自身,所以也有上述特点。如下代码所示:

let arr = [1, 3, 5];
let iterable = arr.values();
let arr_new = [...iterable];      // arr_new = [1, 3, 5]
let arr_latest = [...iterable];   // arr_latest = []
let isSame = iterable[Symbol.iterator]() === iterable;    // isSame = true

处理这种情况,建议将这些方法的返回值作为 立即值 使用,如下所示:

let arr = [1, 3, 5];
let arr_new = [...iterable.values()];      // arr_new = [1, 3, 5]
let arr_latest = [...iterable.values()];   // arr_latest = [1, 3, 5]