迭代器和生成器

对集合中的任意项进行处理是很常见的操作。JavaScript 提供了好几种方法来遍历一个集合,从简单的 for  循环至 map()filter() 。 迭代器和生成器将迭代的概念纳入的javascritp的核心语言中,并提供了一种自定义 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...in 循环结构可以直接取代next()方法的调用。 当StopIteration exception异常抛出时这个循环会自动终止。

var it = Iterator(lang);
for (var pair in it)
  print(pair); // prints each [key, value] pair in turn

假如我们只想遍历一个对象的 keys 时,我们可以通过把第二个参数设置为 true 来实现:

var it = Iterator(lang, true);
for (var key in it)
  print(key); // prints each key in turn

使用 Iterator() 来访问对象内容的优点是对于那些已经加入对象的自定义属性(不用管属性名,一股脑的访问,或者只访问属性,上面就是这样)。它的原型不会包含在序列中。

Iterator() 也可以用于数组:

var langs = ['JavaScript', 'Python', 'C++'];
var it = Iterator(langs);
for (var pair in it)
  print(pair); // prints each [index, language] pair in turn

基于对象,通过传递true作为第二个参数,将会导致迭代结果发生数组索引上益(会吗?我也没搞明白,你知道的话请告诉我):

var langs = ['JavaScript', 'Python', 'C++'];
var it = Iterator(langs, true);
for (var i in it)
  print(i); // prints 0, then 1, then 2

也可以指定块作用域变量(let是个javascript1.7的关键字,可以使用 let 建立只存在于 for 循环上下文中的变量)包括索引和值(index and value)在for…in循环中使用let关键字和非结构化赋值:

var langs = ['JavaScript', 'Python', 'C++'];
var it = Iterator(langs);
for (let [i, lang] in it)
 print(i + ': ' + lang); // prints "0: JavaScript" etc.

定义自定义迭代器

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

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

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

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

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

现在我们要创建一个遍历器,可以返回这个序列中的一序列包容性的整数,这个遍历器接口要求我们我们实现一个next()方法,这个next()方法返回这个序列或者抛出一个StopIteration exception异常。

function RangeIterator(range){
  this.range = range;
  this.current = this.range.low;
}
RangeIterator.prototype.next = function(){
  if (this.current > this.range.high)
    throw StopIteration;
  else
    return this.current++;
};

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

最后,为了RangeIterator与Range序列对象,我们需要给Range写一个特殊的__iterator__方法,当我们试着遍历这个Range序列时会调用这个方法,并且应该返回一个RangeIterator遍历器实例。

Range.prototype.__iterator__ = function(){
  return new RangeIterator(this);
};

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

var range = new Range(3, 5);
for (var i in range)
  print(i); // prints 3, then 4, then 5 in sequence

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

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

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

Note: 这个yield关键字只能被包含在下面的代码快中才有用 <script type="application/javascript;version=1.7">  (或者更高版本).XUL脚本标记访问这些功能不需要这个特殊的块。

当一个生成器函数被一个函数体调用时并不会马上执行;相反,它会返回一个generator-iterator对象。每次调用generator-iterator的next()方法将会执行这个函数体至下一个yield表达式并且返回一个结果。当函数执行完或者执行到一个return时抛出一个StopIteration exception异常。

用一个案例来阐述:

function simpleGenerator(){
  yield "first";
  yield "second";
  yield "third";
  for (var i = 0; i < 3; i++)
    yield i;
}

var g = simpleGenerator();
print(g.next()); // prints "first"
print(g.next()); // prints "second"
print(g.next()); // prints "third"
print(g.next()); // prints 0
print(g.next()); // prints 1
print(g.next()); // prints 2
print(g.next()); // StopIteration is thrown

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

function Range(low, high){
  this.low = low;
  this.high = high;
}
Range.prototype.__iterator__ = function(){
  for (var i = this.low; i <= this.high; i++)
    yield i;
};
var range = new Range(3, 5);
for (var i in range)
  print(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();
print(sequence.next()); // 1
print(sequence.next()); // 1
print(sequence.next()); // 2
print(sequence.next()); // 3
print(sequence.next()); // 5
print(sequence.next()); // 8
print(sequence.next()); // 13

生成器可以有参数,但被限制在函数第一次调用时。生成器可以通过一个return语句来终止(因为他们将抛出一个topIteration exception)。下面是一个变异的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;
  }
}

高级生成器

生成器按照要求产生values,这样可以高效的表示序列,这点对于计算机来说很重要,上面的无穷序列就是一个很好的证明。

除了next()方法,generator-iterator对象也有一个send()方法可以用来修改生成器的内部状态。传递一个值给send()将被视为最后一个yield表达式的结果(生成器暂停)。你必须在你使用send(参数)前通过调用next()来启动生成器。

这里是斐波那契生成器使用send()来重新启动序列:

function fibonacci(){
  var fn1 = 1;
  var fn2 = 1;
  while (1){
    var current = fn2;
    fn2 = fn1;
    fn1 = fn1 + current;
    var reset = yield current;
    if (reset){
        fn1 = 1;
        fn2 = 1;
    }
  }
}

var sequence = fibonacci();
print(sequence.next());     // 1
print(sequence.next());     // 1
print(sequence.next());     // 2
print(sequence.next());     // 3
print(sequence.next());     // 5
print(sequence.next());     // 8
print(sequence.next());     // 13
print(sequence.send(true)); // 1
print(sequence.next());     // 1
print(sequence.next());     // 2
print(sequence.next());     // 3
注意:有意思的一点是,调用 send(undefined) 和调用 next() 是完全同等的。不过,当调用 send() 方法启动一个新的生成器时,除了 undefined 其它的值都会抛出一个 TypeError 异常。.

你可以调用 throw 方法并且传递一个它应该抛出的异常值来强制生成器抛出一个异常。此异常将从当前上下文抛出并暂停生成器,类似当前的 yield 执行,只不过换成了 throw value 语句。

如果在抛出异常的处理过程中没有遇到 yield ,该异常将会被传递直到调用 throw() 方法,并且随后调用 next() 将会导致 StopIteration 异常被抛出。

生成器拥有一个 close() 方法来强制生成器结束。结束一个生成器会产生如下影响:

  1. 所有生成器中有效的 finally 字句将会执行
  2. 如果 finally 字句抛出了除 StopIteration 以外的任何异常,该异常将会被传递到 close() 方法的调用者
  3. 生成器会终止

生成器表达式

JavaScript 1.8 引入

数组推导式的一个明显缺点是,它们会导致整个数组在内存中构造。当输入到推导式的本身是个小数组时它的开销是微不足道的--但是,当输入数组很大或者创建一个新的昂贵(或者是无限的)数组生成器时就可能出现问题。

生成器允许对序列延迟计算(lazy computation),在需要时按需计算元素。生成器表达式在句法上几乎和数组推导式相同--它用圆括号来代替方括号(而且用 for...in 代替 for each...in)--但是它创建一个生成器而不是数组,这样就可以延迟计算。你可以把它想象成创建生成器的简短语法。

假设我们有一个迭代器 it 来迭代一个巨大的整数序列。我们需要创建一个新的迭代器来迭代偶数。一个数组推导式将会在内存中创建整个包含所有偶数的数组:

var doubles = [i * 2 for (i in it)];

而生成器表达式将会创建一个新的迭代器,并且在需要的时候按需来计算偶数值:

var it2 = (i * 2 for (i in it));
print(it2.next()); // The first value from it, doubled
print(it2.next()); // The second value from it, doubled

当一个生成器被用做函数的参数,圆括号被用做函数调用,意味着最外层的圆括号可以被省略:

var result = doSomething(i * 2 for (i in it));

文档标签和贡献者

 此页面的贡献者: ianfung1998, liadbiz, snandy, teoli, ziyunfei, AriesDevil, Joyce
 最后编辑者: ianfung1998,