本文深入解析 JavaScript 中的迭代机制,涵盖 Iterator 接口的使用、自定义迭代器的实现,以及生成器(Generator)的高级特性。通过 Range 和 Fibonacci 序列等代码示例,展示如何利用 yield 关键字简化状态维护,并介绍 send、throw 和 close 等高级控制方法,帮助开发者高效处理集合遍历与延迟计算。
迭代机制的演进
遍历集合中的元素是编程中的常见需求。JavaScript 提供了多种迭代方式,包括基础的 for 和 for each 循环,以及 map()、filter() 和数组推导式等高级方法。自 JavaScript 1.7 版本起,迭代器(Iterator)和生成器(Generator)被引入核心语法,不仅提供了全新的迭代机制,还允许开发者定制 for…in 和 for each 循环的行为。
理解迭代器(Iterator)
迭代器
迭代器是一个用于访问集合序列中单个元素的对象,它能够追踪当前在序列中的迭代位置。在 JavaScript 中,迭代器表现为一个包含 next() 方法的对象,调用该方法可获取序列中的下一个元素。当序列中的所有元素遍历完毕后,next() 方法会抛出 StopIteration 异常以表示结束。
一旦迭代器对象被创建,既可以通过显式重复调用 next() 方法来手动遍历,也可以利用 JavaScript 的 for…in 或 for each 循环来隐式调用。
可以使用 Iterator() 函数轻松创建针对普通对象和数组的迭代器:
代码如下:
var lang = { name: 'JavaScript', birthYear: 1995 };
var it = Iterator(lang);
初始化完成后,可以通过调用 next() 方法依次访问对象的键值对:
代码如下:
var pair = it.next(); //键值对是["name", "JavaScript"]
pair = it.next(); //键值对是["birthday", 1995]
pair = it.next(); //一个 `StopIteration` 异常被抛出
可以使用 for…in 循环替代显式的 next() 调用。当 next() 抛出 StopIteration 异常时,循环会自动终止。
代码如下:
var it = Iterator(lang);
for (var pair in it)
print(pair); //每次输出 it 中的一个 [key, value] 键值对
如果仅需迭代对象的键名,可以向 Iterator() 函数传入第二个参数 true:
代码如下:
var it = Iterator(lang, true);
for (var key in it)
print(key); //每次输出 key 值
使用 Iterator() 访问对象的一个优势在于,挂载在 Object.prototype 上的自定义属性不会包含在迭代序列中。
Iterator() 同样适用于数组:
代码如下:
var langs = ['JavaScript', 'Python', 'Haskell'];
var it = Iterator(langs);
for (var pair in it)
print(pair); //每次迭代输出 [index, language] 键值对
与遍历对象类似,若将 true 作为第二个参数传入,迭代结果将仅包含数组索引:
代码如下:
var langs = ['JavaScript', 'Python', 'Haskell'];
var it = Iterator(langs, true);
for (var i in it)
print(i); //输出 0,然后是 1,然后是 2
利用 let 关键字可以在循环内部分别将索引和值赋给块级变量,同时支持解构赋值(Destructuring Assignment):
代码如下:
var langs = ['JavaScript', 'Python', 'Haskell'];
var it = Iterators(langs);
for (let [i, lang] in it)
print(i + ': ' + lang); //输出 "0: JavaScript" 等
声明自定义迭代器
声明自定义迭代器
某些代表元素集合的对象应当以特定的方式进行迭代。
例如: 1. 表示范围(Range)的对象应逐个返回范围内的数字。 2. 树的叶子节点可通过深度优先或广度优先方式访问。 3. 代表数据库查询结果的对象应逐行返回结果,即使整个结果集尚未全部加载到单一数组中。 4. 作用于无限数学序列(如斐波那契序列)的迭代器应在不创建无限长度数据结构的前提下逐个返回结果。
JavaScript 允许编写自定义迭代逻辑的代码,并将其应用于对象上。
我们创建一个简单的 Range 对象,包含 low 和 high 两个属性:
代码如下:
function Range(low, high){
this.low = low;
this.high = high;
}
接下来创建一个自定义迭代器,返回包含范围内所有整数的序列。迭代器接口要求提供一个 next() 方法,用于返回序列中的下一个元素或抛出 StopIteration 异常。
代码如下:
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 通过 range 实例进行初始化,同时维护一个 current 属性来跟踪当前序列的位置。
最后,为了让 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); //输出 3,然后 4,然后 5
生成器:更优的迭代器构建方式
生成器:一种更优的迭代器构建方式
虽然自定义迭代器是有用的工具,但创建时需仔细规划,因为需要显式维护其内部状态。
生成器提供了强大的功能:它允许定义包含自有迭代算法的函数,并自动维护其状态。
生成器是作为迭代器工厂的特殊函数。如果函数包含一个或多个 yield 表达式,则称其为生成器(注:在 Node.js 中需在函数名前加 * 表示)。
注意:仅在 HTML 中包含在 <script type="application/javascript;version=1.7">(或更高版本)中的代码块才可使用 yield 关键字。XUL (XML User Interface Language) 脚本标签无需指定此特殊代码块即可访问这些特性。
当生成器函数被调用时,函数体不会立即执行,而是返回一个 generator-iterator 对象。每次调用 generator-iterator 的 next() 方法,函数体将执行至下一个 yield 表达式,并返回其结果。当函数结束或遇到 return 语句时,将抛出 StopIteration 异常。
通过一个示例来更好地说明:
代码如下:
function simpleGenerator(){
yield "first";
yield "second";
yield "third";
for (var i = 0; i < 3; i++)
yield i;
}
var g = simpleGenerator();
print(g.next()); //输出 "first"
print(g.next()); //输出 "second"
print(g.next()); //输出 "third"
print(g.next()); //输出 0
print(g.next()); //输出 1
print(g.next()); //输出 2
print(g.next()); //抛出 StopIteration 异常
生成器函数可直接作为类的 __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); //输出 3,然后 4,然后 5
并非所有生成器都会终止,你可以创建代表无限序列的生成器。下面的生成器实现了斐波那契序列,即每个元素都是前两个元素之和:
代码如下:
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 语句终止(即抛出 StopIteration 异常)。下面的 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;
}
}
生成器高级特性
生成器高级特性
生成器可根据需求计算 yield 返回值,这使其能够表示以前昂贵的序列计算需求,甚至是上述无限序列。
除了 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. 生成器终止。
生成器表达式
生成器表达式
数组推导式的一个明显缺点是,它们会在内存中构造整个数组。当输入推导式的本身是小数组时,其开销微不足道;但当输入数组很大,或创建一个新的昂贵(或无限)数组生成器时,可能会出现问题。
生成器允许对序列进行延迟计算(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()); //it 里面的第一个偶数
print(it2.next()); //it 里面的第二个偶数
当生成器被用作函数参数时,圆括号用作函数调用,意味着最外层的圆括号可以省略:
代码如下:
var result = doSomething(i * 2 for (i in it));End.