JavaScript中的迭代器和生成器详解


Posted in Javascript onOctober 29, 2014

处理集合里的每一项是一个非常普通的操作,JavaScript提供了许多方法来迭代一个集合,从简单的for和for each循环到 map(),filter() 和 array comprehensions(数组推导式)。在JavaScript 1.7中,迭代器和生成器在JavaScript核心语法中带来了新的迭代机制,而且还提供了定制 for…in 和 for each 循环行为的机制。

迭代器

迭代器是一个每次访问集合序列中一个元素的对象,并跟踪该序列中迭代的当前位置。在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() 方法。当 StopIteration 异常被抛出时,循环会自动终止。

 var it = Iterator(lang);

    for (var pair in it)

      print(pair); //每次输出 it 中的一个 [key, value] 键值对

如果你只想迭代对象的 key 值,可以往 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 对象,包含低和高两个值:

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

生成器函数可以带有参数,并且会在第一次调用函数时使用这些参数。生成器可以被终止(引起它抛出 StopIteration 异常)通过使用 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;

      }

    }

生成器高级特性

生成器可以根据需求计算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.

Javascript 相关文章推荐
一个不错的应用,用于提交获取文章内容,不推荐用
Mar 03 Javascript
javascript Array.prototype.slice使用说明
Oct 11 Javascript
推荐40个非常优秀的jQuery插件和教程【系列三】
Nov 09 Javascript
jquery动态加载js三种方法实例
Aug 03 Javascript
jquery的each方法使用示例分享
Mar 25 Javascript
jquery跟js初始化加载的多种方法及区别介绍
Apr 02 Javascript
bootstrapfileinput实现文件自动上传
Nov 08 Javascript
微信小程序 小程序制作及动画(animation样式)详解
Jan 06 Javascript
JS小数转换为整数的方法分析
Jan 07 Javascript
vue-cli 打包后提交到线上出现 &quot;Uncaught SyntaxError:Unexpected token&quot; 报错
Nov 06 Javascript
深入了解响应式React Native Echarts组件
May 29 Javascript
javascript canvas检测小球碰撞
Apr 17 Javascript
JS实现倒计时和文字滚动的效果实例
Oct 29 #Javascript
javascript设置连续两次点击按钮时间间隔的方法
Oct 28 #Javascript
jQuery中parents()和parent()的区别分析
Oct 28 #Javascript
原生javascript实现获取指定元素下所有后代元素的方法
Oct 28 #Javascript
JS对象与json字符串格式转换实例
Oct 28 #Javascript
2014年最火的Node.JS后端框架推荐
Oct 27 #Javascript
Dojo Javascript 编程规范 规范自己的JavaScript书写
Oct 26 #Javascript
You might like
使用VisualStudio开发php的图文设置方法
2010/08/21 PHP
PHP以指定字段为索引返回数据库所取的数据数组
2013/06/30 PHP
避免Smarty与CSS语法冲突的方法
2015/03/02 PHP
php 利用array_slice函数获取随机数组或前几条数据
2015/09/30 PHP
由prototype_1.3.1进入javascript殿堂-类的初探
2006/11/06 Javascript
javascript import css实例代码
2008/07/18 Javascript
jQuery hover 延时器实现代码
2011/03/12 Javascript
js判断背景图片是否加载成功使用img的width实现
2013/05/29 Javascript
node.js中的console.dir方法使用说明
2014/12/10 Javascript
JavaScript生成随机数的4种自定义函数分享
2015/02/28 Javascript
javascript异步处理工作机制详解
2015/04/13 Javascript
JavaScript如何调试有哪些建议和技巧附五款有用的调试工具
2015/10/28 Javascript
JS实现控制文本框的内容
2016/07/10 Javascript
jQuery实现点击行选中或取消CheckBox的方法
2016/08/01 Javascript
基于JavaScript实现自动更新倒计时效果
2016/12/19 Javascript
Vue工程模板文件 webpack打包配置方法
2017/12/26 Javascript
jQuery实现的淡入淡出与滑入滑出效果示例
2018/04/18 jQuery
详解webpack模块加载器兼打包工具
2018/09/11 Javascript
jQuery.parseJSON()函数详解
2019/02/28 jQuery
node获取客户端ip功能简单示例
2019/08/24 Javascript
[05:04]完美世界携手游戏风云打造 卡尔工作室地图界面篇
2013/04/23 DOTA
Python的装饰器使用详解
2017/06/26 Python
Python调用ctypes使用C函数printf的方法
2017/08/23 Python
python 基于TCP协议的套接字编程详解
2019/06/29 Python
python实现WebSocket服务端过程解析
2019/10/18 Python
多个python文件调用logging模块报错误
2020/02/12 Python
韩国11街:11STREET
2018/03/27 全球购物
英国排名第一的餐具品牌:Denby Pottery
2019/11/01 全球购物
PatPat香港:婴童服饰和亲子全家装在线购物
2020/09/27 全球购物
成龙洗发水广告词
2014/03/14 职场文书
党的群众路线学习材料
2014/05/16 职场文书
安全生产年活动总结
2014/08/29 职场文书
高中运动会广播稿
2014/09/16 职场文书
人民调解协议书范本
2014/10/11 职场文书
创业计划书之美甲店
2019/09/20 职场文书
Python实现生活常识解答机器人
2021/06/28 Python