迭代器和生成器

处理集合中的每个项是非常常见的操作。JavaScript提供了许多迭代集合的方法,从简单的 for  循环到 map()filter() 。迭代器和生成器将迭代的概念直接带入核心语言,并提供了一种机制来自定义 for...of 循环的行为 。

更多详情,请参见:

迭代器

迭代器是一种为各种不同的数据结构提供统一的访问机制的接口。一个迭代器对象 ,知道如何每次访问集合中的一项, 并记录它的当前在序列中所在的位置。在  JavaScript 中 迭代器是一个对象,提供了一个 next()  方法,返回序列中的下一项。这个方法返回包含 done 和 value 两个属性的对象。

迭代器对象创建后,可以反复调用 next()使用。

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

一旦初始化,next()方法可以用来依次访问对象中的键-值:

var it = makeIterator(['yo', 'ya']);
console.log(it.next().value); // 'yo'
console.log(it.next().value); // 'ya'
console.log(it.next().done);  // true

一个 for...of 循环结构可以直接取代 next() 方法的调用。

// Array 实现了iterator 接口,可以直接使用 for ... of 方法遍历
var langs = ['JavaScript', 'Python', 'C++'];
for (const l of langs) {
  console.log(l);
}
// result:
// JavaScript
// Python
// C++

定义自定义迭代器

某些对象遍历集合的项目时应该使用特定的方式遍历。

  • 遍历一个序列对象时应该是一个接着一个。
  • 遍历一棵树时应该使用深度优先或者广度优先方式。
  • 遍历一个数据库查询结果对象时应该是一条一条地遍历,即使整个结果并没有被加载到一个数组中。
  • 一个迭代器在一个无限的数学序列(如斐波那契序列)应该能够一个一个地返回结果而不创建一个无限长度的数据结构。

JavaScript 可以让你编写代码来表示自定义迭代逻辑并链接到一个对象。

我们来创建一个简单的序列对象用来存放 low 和 high。

function Range(low, high){
  this.low = low;
  this.high = high;
}

现在创建一个迭代器,可以返回这个序列中的一序列包容性的整数,这个遍历器接口要求我们我们实现一个 next() 方法,当序列中还有元素的时候对应的元素{value: currentValue, done: false},当遍历完成后返回 {done: true}。

function RangeIterator(range){
  this.range = range;
  this.current = this.range.low;
}
RangeIterator.prototype.next = function(){
   const nextVal = {done: true};
   if (this.current <= this.range.high) {
       nextVal.value = this.current;
       nextVal.done = false;
       this.current += 1;
   }
    return nextVal;
};

RangeIterator 被一个序列实例所序列化,并且维持它自己当前的属性属性所在位置

最后,我们需要给 Range 添加一个特殊的`[Symbol.iterator]`方法,当我们试着遍历这个 Range 序列时会调用这个方法,并且应该返回一个 RangeIterator 遍历器实例

Range.prototype[Symbol.iterator] = function(){
  return new RangeIterator(this);
};

下面就写段代码来实验下我们自定义的遍历器吧:

var range = new Range(3, 5);
for (var i of range) {
  console.log(i);
}

生成器(Generators): 一个构建遍历器的更好方法

虽然迭代器是一个有用的工具,但是由于需要显式地维持他们的内部状态,所以需要仔细地规划他们的构造(看得我眼花呀)。生成器给你提供了一个强大的选择:它允许你通过写一个可以保存自己状态的的简单函数来定义一个迭代算法。

一个生成器其实是一种特殊类型的函数(这个函数作为一个为迭代器工作的工厂),一个函数如果它里面包含了一个或一个以上的 yield表达式,那么这个函数就成为一个生成器了。

当一个生成器函数被一个函数体调用时并不会马上执行;相反,它会返回一个`generator-iterator`对象。每次调用`generator-iterator`的 next() 方法将会执行这个函数体至下一个 yield 表达式并且返回一个类似`Iterator`的返回结果`{value: any, done: boolean}`。当函数执行完或者执行到一个 return 时返回`{value: undefined, done: true}`。

用一个案例来阐述:

function* simpleGenerator(){
  yield "first";
  yield "second";
  yield "third";
  for (var i = 0; i < 3; i++)
    yield i;
}
var g = simpleGenerator();
console.log(g.next().value); // first
console.log(g.next().value); // second
console.log(g.next().value); // third
console.log(g.next().value); // 0
console.log(g.next().value); // 1
console.log(g.next().value); // 2
console.log(g.next().value); // undefined

一个生成器函数可以直接用作为一个类的`[Symbol.iterator]`方法,大大的减少了创建一个自定义迭代器的代码量,下面是Range重写成一个生成器:

function Range(low, high){
    this.low = low;
    this.high = high;
}
Range.prototype[Symbol.iterator] = function*(){
    for (var i = this.low; i <= this.high; i++) {
        yield i;
    }
};
var range = new Range(3, 5);
for (var i of range) {
  console.log(i); // prints 3, then 4, then 5 in sequence
}

并不是所有的生成器都会终止;也可以创建一个无穷序列的生成器。下面实现了斐波纳契数列的生成器:

function* fibonacci(){
  var fn1 = 1;
  var fn2 = 1;
  while (1){
    var current = fn2;
    fn2 = fn1;
    fn1 = fn1 + current;
    yield current;
  }
}
var sequence = fibonacci();
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 2
console.log(sequence.next().value); // 3
console.log(sequence.next().value); // 5
console.log(sequence.next().value); // 8
console.log(sequence.next().value); // 13
// ... 

生成器可以有参数,但被限制在函数第一次调用时。生成器可以通过一个 return 语句来终止。下面是一个变异的 fibonacci 函数,有一个 limit 参数,一旦满足条件将终止。

function* fibonacci(limit){
  var fn1 = 1;
  var fn2 = 1;
  while (1){
    var current = fn2;
    fn2 = fn1;
    fn1 = fn1 + current;
    if (limit && current > limit) return;
    yield current;
  }
}
var sequence = fibonacco(15);
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 2
console.log(sequence.next().value); // 3
console.log(sequence.next().value); // 5
console.log(sequence.next().value); // 8
console.log(sequence.next().value); // 13
console.log(sequence.next().value); // undefined

虽然生成器只能在第一次调用的时候传递参数,但是 next 方法也可以接受参数,因此我们可以利用 next 向生成器内部继续注入值:

function* fibonacco() {
  var fn1 = fn2 = 1;
  while(1) {
    var current = fn2;
    fn2 = fn1;
    fn1 = fn1 + current;
    var reset = yield current;
    if (reset) {
      fn1 = fn2 = 1;
    }
  }
}
var sequence = fibonacco(15);
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 1
console.log(sequence.next().value); // 2
console.log(sequence.next().value); // 3
console.log(sequence.next().value); // 5
console.log(sequence.next().value); // 8
console.log(sequence.next().value); // 13
console.log(sequence.next(true).value); // 1

 

文档标签和贡献者