JavaScript中的迭代器和生成器详解
处理集合里的每一项是一个非常普通的操作,JavaScript提供了许多方法来迭代一个集合,从简单的for和foreach循环到map(),filter()和arraycomprehensions(数组推导式)。在JavaScript1.7中,迭代器和生成器在JavaScript核心语法中带来了新的迭代机制,而且还提供了定制for…in和foreach循环行为的机制。
迭代器
迭代器是一个每次访问集合序列中一个元素的对象,并跟踪该序列中迭代的当前位置。在JavaScript中迭代器是一个对象,这个对象提供了一个next()方法,next()方法返回序列中的下一个元素。当序列中所有元素都遍历完成时,该方法抛出StopIteration异常。
迭代器对象一旦被建立,就可以通过显式的重复调用next(),或者使用JavaScript的for…in和foreach循环隐式调用。
简单的对对象和数组进行迭代的迭代器可以使用Iterator()被创建:
varlang={name:'JavaScript',birthYear:1995}; varit=Iterator(lang);
一旦初始化完成,next()方法可以被调用来依次访问对象的键值对:
varpair=it.next();//键值对是["name","JavaScript"] pair=it.next();//键值对是["birthday",1995] pair=it.next();//一个`StopIteration`异常被抛出
for…in循环可以被用来替换显式的调用next()方法。当StopIteration异常被抛出时,循环会自动终止。
varit=Iterator(lang); for(varpairinit) print(pair);//每次输出it中的一个[key,value]键值对
如果你只想迭代对象的key值,可以往Iterator()函数中传入第二个参数,值为true:
varit=Iterator(lang,true); for(varkeyinit) print(key);//每次输出key值
使用Iterator()访问对象的一个好处是,被添加到Object.prototype的自定义属性不会被包含在序列对象中。
Iterator()同样可以被作用在数组上:
varlangs=['JavaScript','Python','Haskell']; varit=Iterator(langs); for(varpairinit) print(pair);//每次迭代输出[index,language]键值对
就像遍历对象一样,把true当做第二个参数传入遍历的结果将会是数组索引:
varlangs=['JavaScript','Python','Haskell']; varit=Iterator(langs,true); for(variinit) print(i);//输出0,然后是1,然后是2
使用let关键字可以在循环内部分别分配索引和值给块变量,还可以解构赋值(DestructuringAssignment):
varlangs=['JavaScript','Python','Haskell']; varit=Iterators(langs); for(let[i,lang]init) print(i+':'+lang);//输出"0:JavaScript"等
声明自定义迭代器
一些代表元素集合的对象应该用一种指定的方式来迭代。
1.迭代一个表示范围(Range)的对象应该一个接一个的返回这个范围包含的数字
2.一个树的叶子节点可以使用深度优先或者广度优先访问到
3.迭代一个代表数据库查询结果的对象应该一行一行的返回,即使整个结果集尚未全部加载到一个单一数组
4.作用在一个无限数学序列(像斐波那契序列)上的迭代器应该在不创建无限长度数据结构的前提下一个接一个的返回结果
JavaScript允许你写自定义迭代逻辑的代码,并把它作用在一个对象上
我们创建一个简单的Range对象,包含低和高两个值:
functionRange(low,high){ this.low=low; this.high=high; }
现在我们创建一个自定义迭代器,它返回一个包含范围内所有整数的序列。迭代器接口需要我们提供一个next()方法用来返回序列中的下一个元素或者是抛出StopIteration异常。
functionRangeIterator(range){ this.range=range; this.current=this.range.low; } RangeIterator.prototype.next=function(){ if(this.current>this.range.high) throwStopIteration; else returnthis.current++; };
我们的RangeIterator通过range实例来实例化,同时维持一个current属性来跟踪当前序列的位置。
最后,为了让RangeIterator可以和Range结合起来,我们需要为Range添加一个特殊的__iterator__方法。当我们试图去迭代一个Range时,它将被调用,而且应该返回一个实现了迭代逻辑的RangeIterator实例。
Range.prototype.__iterator__=function(){ returnnewRangeIterator(this); };
完成我们的自定义迭代器后,我们就可以迭代一个范围实例:
varrange=newRange(3,5); for(variinrange) print(i);//输出3,然后4,然后5
生成器:一种更好的方式来构建迭代器
虽然自定义的迭代器是一种很有用的工具,但是创建它们的时候要仔细规划,因为需要显式的维护它们的内部状态。
生成器提供了很强大的功能:它允许你定义一个包含自有迭代算法的函数,同时它可以自动维护自己的状态。
生成器是可以作为迭代器工厂的特殊函数。如果一个函数包含了一个或多个yield表达式,那么就称它为生成器(译者注:Node.js还需要在函数名前加*来表示)。
注意:只有HTML中被包含在<scripttype="application/javascript;version=1.7">(或者更高版本)中的代码块才可以使用yield关键字。XUL(XMLUserInterfaceLanguage)脚本标签不需要指定这个特殊的代码块也可以访问这些特性。
当一个生成器函数被调用时,函数体不会即刻执行,它会返回一个generator-iterator对象。每次调用generator-iterator的next()方法,函数体就会执行到下一个yield表达式,然后返回它的结果。当函数结束或者碰到return语句,一个StopIteration异常会被抛出。
用一个例子来更好的说明:
functionsimpleGenerator(){ yield"first"; yield"second"; yield"third"; for(vari=0;i<3;i++) yieldi; } varg=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:
functionRange(low,high){ this.low=low; this.high=high; } Range.prototype.__iterator__=function(){ for(vari=this.low;i<=this.high;i++) yieldi; }; varrange=newRange(3,5); for(variinrange) print(i);//输出3,然后4,然后5
不是所有的生成器都会终止,你可以创建一个代表无限序列的生成器。下面的生成器实现一个斐波那契序列,就是每一个元素都是前面两个的和:
functionfibonacci(){ varfn1=1; varfn2=1; while(1){ varcurrent=fn2; fn2=fn1; fn1=fn1+current; yieldcurrent; } } varsequence=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
生成器函数可以带有参数,并且会在第一次调用函数时使用这些参数。生成器可以被终止(引起它抛出StopIteration异常)通过使用return语句。下面的fibonacci()变体带有一个可选的limit参数,当条件被触发时终止函数。
functionfibonacci(limit){ varfn1=1; varfn2=1; while(1){ varcurrent=fn2; fn2=fn1; fn1=fn1+current; if(limit&¤t>limit) return; yieldcurrent; } }
生成器高级特性
生成器可以根据需求计算yield返回值,这使得它可以表示以前昂贵的序列计算需求,甚至是上面所示的无限序列。
除了next()方法,generator-iterator对象还有一个send()方法,该方法可以修改生成器的内部状态。传给send()的值将会被当做最后一个yield表达式的结果,并且会暂停生成器。在你使用send()方法传一个指定值前,你必须至少调用一次next()来启动生成器。
下面的斐波那契生成器使用send()方法来重启序列:
functionfibonacci(){ varfn1=1; varfn2=1; while(1){ varcurrent=fn2; fn2=fn1; fn1=fn1+current; varreset=yieldcurrent; if(reset){ fn1=1; fn2=1; } } } varsequence=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执行,只不过换成了throwvalue语句。
如果在抛出异常的处理过程中没有遇到yield,该异常将会被传递直到调用throw()方法,并且随后调用next()将会导致StopIteration异常被抛出。
生成器拥有一个close()方法来强制生成器结束。结束一个生成器会产生如下影响:
1.所有生成器中有效的finally字句将会执行
2.如果finally字句抛出了除StopIteration以外的任何异常,该异常将会被传递到close()方法的调用者
3.生成器会终止
生成器表达式
数组推导式的一个明显缺点是,它们会导致整个数组在内存中构造。当输入到推导式的本身是个小数组时它的开销是微不足道的—但是,当输入数组很大或者创建一个新的昂贵(或者是无限的)数组生成器时就可能出现问题。
生成器允许对序列延迟计算(lazycomputation),在需要时按需计算元素。生成器表达式在句法上几乎和数组推导式相同—它用圆括号来代替方括号(而且用for...in代替foreach...in)—但是它创建一个生成器而不是数组,这样就可以延迟计算。你可以把它想象成创建生成器的简短语法。
假设我们有一个迭代器it来迭代一个巨大的整数序列。我们需要创建一个新的迭代器来迭代偶数。一个数组推导式将会在内存中创建整个包含所有偶数的数组:
vardoubles=[i*2for(iinit)];
而生成器表达式将会创建一个新的迭代器,并且在需要的时候按需来计算偶数值:
varit2=(i*2for(iinit)); print(it2.next()); //it里面的第一个偶数 print(it2.next()); //it里面的第二个偶数
当一个生成器被用做函数的参数,圆括号被用做函数调用,意味着最外层的圆括号可以被省略:
varresult=doSomething(i*2for(iinit));
End.