J**aScript中的迭代器和生成器详解

时间:2016.04.21 发布人:FFFF_123

J**aScript中的迭代器和生成器详解

已解决问题

谷歌FFFF_123用户在2016.04.21提交了关于“格蓝迪J**aScript中的迭代器和生成器详解”的提问,欢迎大家涌跃发表自己的观点。目前共有1个回答,最后更新于2024-12-04T07:32:11。希望大家能够帮助她。

详细问题描述及疑问:期待您的答案,真心佩服你,谢谢 !

希望以下的回答,能够帮助你。

第1个回答

用户名:作鞋委员会  

处理集合里的每一项是一个非常普通的操作,J**aScript提供了许多方法来迭代一个集合,从简单的for和foreach循环到map(),filter()和arra备斗击氧围征架胡且犯氢ycomprehensions(数组推导式)。在J**aScript1.7中,迭代器和生成器在J**aScript核心语法中带来了新的迭代机制,而且还提供了定制for…in和foreach烟具剂循环行为的机制。

迭代器

迭代器是一个每次访问集合序列中一个元素的对象,并跟踪该序列中迭代的问答当前位置。在J**aScript中迭代器是一个对象,这个对象提供了一个next()方法,next()方法返回序列中的下一个元素。当序列中所有元素都遍历完成时,该方法抛出StopIteration异常。

迭代器对象一旦被建立,就可以通过显式的重复调用next(),或者使用J**aScript的for…in和foreach循环隐式调用。

简单的对对象和数组进行迭代的迭代器可以使用Iterator()被创建:

代码如下:
varlang={name:'J**aScript',birthYear:1995};
varit=Iterator(lang);

一旦初始化完成,next()方法可以被调用来依次访问对象的键衷:

代码如下:
varpair=***.next();//键衷是["name","J**aScript"]
pair=***.next();//键衷是["birthday",1995]
pair=**于整整编天诉色室过*.next();//一个`StopIteration`异常被抛出

for…in循环可情吸医以被用来替换显式的调用next()方法呼继问毫毫向。当StopIteration异常被抛出时,循环会自动终止。

代码如下:
va年rit=Iterator(lang);
for(varpairinit)
print(pair);//每次输出it中的一个[key,value]键衷

如果你只想迭代对象的key值,可以往Iterator()函数中传入第二个参数,值为true:

代码如下:
va值担农气能烧导开愿rit=Iterator(lang,true);
for(varkeyinit)
pr活具庆结杀延int(key);//每次输出key值

使用Iterator()访问对象的一个好处是,被添加到Object.prototype的自定义属性不会被包含在序列对象中。

Iterator()同样可以被作用在数组上:

代码如下:
var普三类团握试终触亮langs=['J**aScript','Python','h**askell'];
varit=Iterator(langs);
for(varpairinit)
print(pair);//每次迭代输出[index,language]键衷

就像遍历对象一样,把true当做第二个参数传入遍历的结果将会是数组索引:

varlangs=['J**aScript','Python','h**站汽askell'];
varit=Iterator(langs,true);
for(variinit)
print(i);//输出0,然后是1,然后是2

使用let关往令病面鲜的业缺体太键字可以在循环**分别分配索引和值给块变量,还可以解构赋值(志面阶交解总陈使DestructuringAssignment):

代码如下:
varlangs=['J**aScript','Pytho风问展生面云开皮否n','h**askell'];
varit=Iterators(langs);
f衣or(let[i,lang]init)
print(i+':'+lang)放架要;//输出"0:J**aScript"等

声明自定义迭代器

些代表元素集合的对象应该用一种指定的方式来迭代。

1.迭代一个表示范围(元Range)的对象应该使决首给然举探吧一个接一个的返回这个范围包含的数字
2.一个树的叶子节点可以使用深度优先或者广度优先访问到
3.迭代一个代表**库查询结果的对象应该一行一行的返回,即使整个结果集尚未全部加载到一个单一数组
4.作用在一个无限数学序列(像斐波那契序列)上的迭代器应该在不创建无限长度**结构的前提下一个接一个的返回结果

J**aScript允许你写自定义迭代逻辑的代码,并把它作用在一个对象上

我们创建一个简单的Range对象,包含低和高两个值:

代码如下:
functionRange(low,**){
this.low=low;
this.**=**;
}

现在我们创建一个自定义迭代器,它返回一个包含范围内所有整数的序列。迭代器接口需要我们提供一个next()方法用来返回序列中的下一个元素或者是抛出StopIteration异常。

代码如下:
functionRangeIterator(range){
this.range=range;
this.current=this.range.low;
}
RangeIterator.***.next=function(){
if(this.current>this.range.**)
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还需要在函数名前加*来表示)。

注意:只有h**TML中被包含在<scripttype="application/j**ascript;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,**){
this.low=low;
this.**=**;
}
Range.prototype.__iterator__=function(){
for(vari=this.low;i<=this.**;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(***.next());//1
print(***.next());//1
print(***.next());//2
print(***.next());//3
print(***.next());//5
print(***.next());//8
print(***.next());//13

生成器函数可以带有参数,并且会在第一次调用函数时使用这些参数。生成器可以被终止(引起它抛出StopIteration异常)通过使用return语句。下面的fibonacci()变体带有一个可选的limit参数,当条件被触发时终止函数。

代码如下:
functionfibonacci(limit){
varfn1=1;
varfn2=1;
while(1){
varcurrent=fn2;
fn2=fn1;
fn1=fn1+current;
if(limit&&current>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(***.next());//1
print(***.next());//1
print(***.next());//2
print(***.next());//3
print(***.next());//5
print(***.next());//8
print(***.next());//13
print(sequence.send(true));//1
print(***.next());//1
print(***.next());//2
print(***.next());//3

注意:有意思的一点是,调用send(undefined)和调用next()是完全同等的。不过,当调用send()方法启动一个新的生成器时,除了undefined其它的旨会抛出一个TypeError异常。

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

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

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

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

生成器表达式

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

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

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

代码如下:
vardoubles=[i*2for(iinit)];

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

代码如下:
varit2=(i*2for(iinit));
print(***.next());//it里面的第一个偶数
print(***.next());//it里面的第二个偶数

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

代码如下:
varresult=doSomething(i*2for(iinit));