JS魔法堂:ES6新特性——GeneratorFunction介绍

简介:

一、前言                                

  第一次看koajs的示例时,发现该语句 function *(next){...............} ,这是啥啊?于是搜索一下,原来这是就是ES6的新特性Generator Function(生成器函数)。

  那什么是生成器函数呢?其实就相当于C#2.0中通过yield关键字实现的迭代器的生成器(细节有所不同),那么理解的关键就在迭代器和yield关键字两部分了。下面将尝试从表象出发,逐步对生成器函数及利用它进行异步编程进行浅层的分析理解。

 

二、表象——语法及基本使用                     

  示例:

复制代码
// 定义生成器函数
function *enumerable(msg){
  console.log(msg)
  var msg1 = yield msg + '  after '
  console.log(msg1)
  var msg2 = yield msg1 + ' after'
  try{
    var msg3 = yield msg2 + 'after'
    console.log('ok')
  }
  catch(e){
    console.log(e)
  }
  console.log(msg2 + ' over')
}

// 初始化迭代器
var enumerator = enumerable('hello')
var ret = enumerator.next() // 控制台显示 hello,ret的值{value:'hello after',done:false}
ret =  enumerator.next('world') // 控制台显示 world,ret的值{value:'world after',done:false}
ret = enumerator.next('game') // 控制台显示game,ret的值{value:'game after',done:false}
// 抛出异常信息
ret = enumerator.throw(new Error('test')) // 控制台显示new Error('test')信息,然后显示game over。ret的值为{done:true}

// for...of语句
enumerator = enumerable('hello')
for(ret of enumerator)
  console.log(JSON.stringify(ret));
// 控制台依次显示
// hello
// {value:'hello after',done:false}
// world
// {value:'world after',done:false}
// {value:'game after',done:false}
// game over
// {done:true}
复制代码

 

  1. 生成器语函数定义

function* test(){}
function * test(){}
function *test(){}
test = function* (){} 
test = function *(){}

  普通函数添加*号后则成为了成为了生成器函数了。

Object.prototype.toString.call(test) // 显示[object GeneratorFunction]

  生成器函数的行为与普通函数并不相同,表现为如下3点:

  1. 通过new运算符或函数调用的形式调用生成器函数,均会返回一个生成器实例;

  2. 通过new运算符或函数调用的形式调用生成器函数,均不会马上执行函数体的代码;

  3. 必须调用生成器实例的next方法才会执行生成器函数体的代码。

复制代码
function *say(msg){
  console.log(msg)
}
var gen = say('hello world') // 没有显示hello world
console.log(Object.prototype.toString.call(gen)) // 显示[object Generator]
gen.next() // 显示hello world
复制代码

  2、 关键字yield——迭代器生成器

   用于马上退出代码块并保留现场,当执行迭代器的next函数时,则能从退出点恢复现场并继续执行下去。下面有2点需要注意:

    1. yield后面的表达式将作为迭代器next函数的返回值;

    2. 迭代器next函数的入参将作为yield的返回值(有点像运算符)。

  3、迭代器(Generator)

    迭代器是一个拥有 {value:{*}, done:{Boolean}} next([*])方法 和 {undefined} throw([*])方法 的对象,通过next函数不断执行以关键字yield分割的代码段,通过throw函数令yield分割的代码段抛出异常。

 

三、核心1——迭代器                      

  迭代器更多的是指迭代器模式,迭代器模式是指通过一个名为迭代器的对象按一定的规则遍历集合元素,调用者只需告诉迭代器获取下一个元素即可,而集合的类型、如何获取元素等因素均由具体的迭代器自行处理。(又一次地关注点分离!)并且由于迭代器模式可以做到 按需执行/延迟执行 的效果,因此能降低遍历无限序列时内存/栈溢出的问题,也能作为异步编程模式使用。

  模式理解的注意点:

      1. 迭代器每次进访问集合的一个元素,并由调用者发起访问请求时迭代器才执行下一次访问操作

      2. “按一定的规则”,意味着不一定遍历集合中所有的元素,并且规则可以内聚到迭代器的具体实现上,也可通过策略模式外移到其他模块中;

      3. “集合”,集合可以是一开始就已经初始化好的有限序列集合(如[1,2,3,4,5,6,7]),也可以是按需生成的无限序列集合(如1到无限大)

      4. “集合元素”,可以是整数集合、字符串集合等数据集合,也可以是函数等指令+数据的集合;

  若触过C#、Java等服务端语句的朋友应该对迭代器有一定程度的了解,C#的IEnumrable、IEnumerator和Java的Iterable、Iterator就是跟迭代器相关的接口定义,继承上述接口的迭代器实现均可以通过foreach或for...in语句作循环操作。

  那么这里有2点是要注意的:

      1. 迭代器是指设计模式,跟具体的语言无关,因此所有语言均可根据该模式实现具体的迭代器;

      2. foreach或for...in语句是语法层面的支持,跟迭代器模式没有必然联系。(若语法层面不支持,那函数式编程中的递归的效果是一样的,假如编译器/解析器支持尾递归则更好了,可以JS不支持)

  下面我们通过迭代器来实现Python中的range函数,并通过range函数创建一个超大的有限序列正整数集合(直接用数组的话绝有可能导致栈溢出哦!)。

复制代码
// 迭代器构造函数
var RangeIterator = function(start,end,scan){
    this.start = arguments.length >= 2 ? start : 0    
    this.end = end == undefined ? start : end
    this.scan = scan || 1
    this.idx = this.start
}
// 向迭代器发起访问下一个元素的请求
// FF和ES6下迭代器接口规范定义了迭代器必须通过名为next的函数发起访问下一个元素的请求
RangeIterator.prototype.next = function(){
    if (this.idx > this.end) 
    if (!!StopIteration) {
         throw StopIteration
       }else{
          return void 0
       }

    var ret = this.idx
    this.idx += this.scan
    return ret
}
// Python中的range函数
var range = function(start, end, scan){
   var iterator = new RangeIterator(start, end, scan)
   return {
       // FF下令for...in语句调用对象的迭代器的接口规范
        __iterator__: function(){
            return iterator
        },
       // 暴露迭代器的next函数
        next: function(){
            return iterator.next()
        },
        toString: function(){
            // 可能会导致栈溢出
            var array = []
            for (var i = this.next(); i != void 0; i = this.next())
                array.push(i)
            return array + ''
        }    
    }
}
var r = range(1, 100000000000000000000)
// FF下
// 参考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Iterators_and_Generators#.E5.AE.9A.E4.B9.89.E8.87.AA.E5.AE.9A.E4.B9.89.E8.BF.AD.E4.BB.A3.E5.99.A8
for(var i in r)
  console.log(i) // 显示1到99999999999999999999
// 所有浏览器
for (var i = r.next(); i != void 0; i = r.next())
  console.log(i) // 显示1到99999999999999999999
复制代码

  由于JS是单线程运行,并且当UI线程被阻塞N秒后,浏览器会询问是否停止脚本的执行,但上述代码并不会由于序列过大造成栈溢出的问题。假如预先生成1到99999999999999999999或更大数字的数组,那很有可能造成stack overflow。那是由于迭代器实质为一状态机,而调用next函数则是触发状态的转换,而状态机中同一时刻用于存放变量的存储空间固定,并不会出现无限增长的情况。

 

四、核心2——yield关键字                   

   回到关键字yield上了,其实yield关键字就是以一种更直观、便捷的方式让我们创建用于遍历有限序列集合的迭代器,而yield则用于将生成器函数的代码切片作为有限序列集合的元素(元素的类型为指令+数据,而不仅仅是数据而已)。下面我们一起看看yield关键字是怎样对代码切片的吧!

复制代码
// 定义生成器函数
function *enumerable(msg){
  console.log(msg)
  var msg1 = yield msg + '  after '
  console.log(msg1)
  var msg2 = yield msg1 + ' after'
  console.log(msg2 + ' over')
}
复制代码

  上述代码最终会被解析为下面的代码:

复制代码
var enumerable = function(msg){
  var state = -1
 
  return {
    next: function(val){
      switch(++state){
         case 0:
                  console.log(msg + ' after')
                  break
         case 1:
                  var msg1 = val
                  console.log(msg1 + ' after')
                  break
         case 2:
                  var msg2 = val
                  console.log(msg2 + ' over')
                  break
      }
    }
  }
}
复制代码

(注意:上述仅仅简单的分析,更复杂的情况(条件控制、循环、迭代、异常捕获处理等)可以参考@赵劼的《人肉反编译使用关键字yield的方法》)

 

 五、异步调用中的应用                       

  由于迭代器模式实现 延迟执行/按需执行,因此可作为一种异步编程模式来应用。

复制代码
var iterator = getArticles('dummy.json')
// 开始执行
iterator.next()
// 异步任务模型
function getData(src){
  setTimeout(function(){
    iterator.next({tpl: 'tpl.html', name: 'fsjohnhuang'})
  }, 1000)
}
function getTpl(tpl){
  setTimeout(function(){
    iterator.next('hello ${name}')
  }, 3000)
}
// 同步任务 function render(data, tpl){
return tpl.replace(/\$\{(\w+)\}/, function(){ return data[arguments[1]] == void 0 ? arguments[0] : data[arguments[1]] }) } // 主逻辑 function *getAritcles(src){ console.log('begin') var data = yield getData(src) var tpl = yield getTpl(data.tpl) var res = render(data, tpl) console.log(rest) }
复制代码

  主逻辑中异步调用的写法与同步调用的基本没差异了,爽了吧!但异步任务模型与生成器函数及其生成的迭代器耦合性太大,还是不太好用。下面我们通过实现了Promises/A+规范的Q来进一步解耦。

  若执行引擎不支持关键字yield,那么上述代码不就无法执行了吗?还是那句话,yield关键字其实就是语法糖,最终还是会被解析为一个迭代器。因此我们自行实现一个迭代器也是能实现上述效果的,不过过程会繁琐很多(若如第2节的示例那样存在try...catch语句,就繁琐死了@~@),并且代码的整洁性、可维护性就全靠攻城狮来保证了。(语法糖从语法层面简化编程和维护难度,但理解底层的工作原理也十分重要哦!)

 

六、与Q结合                        

复制代码
// 异步任务模型
function getData(src){
  var deferred = Q.defer()
  setTimeout(function(){
   defer.resolve({tpl: 'tpl.html', name: 'fsjohnhuang'})
  }, 1000)
  return deferred.promise
}
function getTpl(tpl){
  var deferred = Q.defer()
  setTimeout(function(){
   defer.resolve('hello ${name}')
  }, 3000)
  return deferred.promise
}
// 同步任务
function render(data, tpl){
  return tpl.replace(/\$\{(\w+)\}/, function(){
    return data[arguments[1]] ==  void 0 ? arguments[0] : data[arguments[1]]
  })
}

// 主逻辑
Q.async(function *(){
  console.log('begin')
  var data = yield getData('dummy.json')
  var tpl = yield getTpl(data.tpl)
  var res = render(data, tpl)
  console.log(rest)
})
复制代码

  暂未阅读Q的源代码,暂不作详细分析。反正API就这样用,呵呵!

 

七、与iPromise结合                    

  iPromise是我开发的一个Promises/A+的完整实现,阅读源码你会发现它继承了jQuery.Deferred1.5~2.1、jsDeferred、mmDeferred和Promises/A官网实现示例的精妙设计,并且从v0.0.6开始支持ES6特性GeneratorFunction。使用示例如下:

复制代码
var getData = function(dataSrc){
  return iPromise(function(r){
    setTimeout(function(){
        r(dataSrc + ' has loaded')
    }, 1000)
  })
}
var getTpl = function(tplSrc){
  return iPromise(function(r){
    setTimeout(function(){
        r(tplStr + ' has loaded')
    }, 2000)
  })
}
var render = function(data, tpl){
    throw new Error('OMG!')
}
iPromise(function
*(dataSrc, tplSrc){ try{ var data = yield getData(dataSrc) var tpl = yield getTpl(tplSrc) render(data, tpl) } catch(e){ console.log(e) } console.log('over!') }, 'dummyData.json', 'dummyTpl.json') /* 结果如下 */ // 等待1秒多显示 dummyData.json has loaded // 等待2秒多显示 dummyTpl.json has loaded // 显示 Error: OMG! // Stack trace: // test10/render/</<@file:///home/fsjohnhuang/repos/iPromise/test/v0.0.2.html:190:6 // 显示 over!
复制代码

v0.6.0的中通过递归来实现,具体如下(https://github.com/fsjohnhuang/iPromise/blob/master/src/iPromise.js#L76):

复制代码
// FF下生成器函数的入参必须在创建迭代器时传递
// 若第一次调用迭代器的next函数传递参数,则会报TypeError: attempt to send 第一个入参值 to newborn generator
var iterator = mixin.apply(null, toArray(arguments,1))
var next = function(){
  var deferred = iPromise()
  deferred.resolve.apply(deferred, arguments)

  return deferred.then(function(){
    var yieldReturn = iterator.next.apply(iterator, arguments)
     if(yieldReturn.done) throw Error('StopIteration')
                        
     return yieldReturn.value
  }).then(next, function(e){
    iterator.throw(e)
}) } deferred.resolve() deferred.then(next)
复制代码

 

八、总结                          

  Generator Function并不是为异步编程而生,但可以将它结合Promise来实现良好的异步编程模型。本篇内容仅简单介绍Generator Function及相关的异步编程内容,若有纰漏请各位指正,谢谢!

  尊重原创,转载请注明来自:http://www.cnblogs.com/fsjohnhuang/p/4166267.html ^_^肥仔John

 

九、 参考                          

http://huangj.in/765

https://www.imququ.com/post/generator-function-in-es6.html

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/The_Iterator_protocol

https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/function*

http://www.cnblogs.com/yangecnu/archive/2012/03/17/2402432.html

http://www.cnblogs.com/draem0507/p/3795189.html

http://blog.zhaojie.me/2010/06/code-for-fun-iterator-generator-yield-in-javascript-answer-2-loop-and-interpreter.html

http://blog.zhaojie.me/2010/06/code-for-fun-iterator-generator-yield-in-javascript.html

http://blog.zhaojie.me/2010/07/why-java-sucks-and-csharp-rocks-6-yield.html

如果您觉得本文的内容有趣就扫一下吧!捐赠互勉!

posted @ 2014-12-17 00:52 ^_^肥仔John 阅读( 17323) 评论( 2) 编辑 收藏
  
#1楼 2014-12-17 09:22 幻天芒  
我还需要慢慢体会,还不是太明白。
http://pic.cnblogs.com/face/343299/20150320135757.png
  
#2楼 [ 楼主] 3088039 2014/12/17 13:54:00 2014-12-17 13:54 ^_^肥仔John  
@ 幻天芒
刚修改了一下,也许会描述得更清楚些!
http://pic.cnblogs.com/face/347002/20141205140116.png

公告

 
本文转自 ^_^肥仔John博客园博客,原文链接: http://www.cnblogs.com/fsjohnhuang/p/4166267.html,如需转载请自行联系原作者
 
相关文章
|
23天前
|
缓存 JavaScript 数据安全/隐私保护
js开发:请解释什么是ES6的Proxy,以及它的用途。
`ES6`的`Proxy`对象用于创建一个代理,能拦截并自定义目标对象的访问和操作,应用于数据绑定、访问控制、函数调用的拦截与修改以及异步操作处理。
17 3
|
23天前
|
JavaScript
js开发:请解释什么是ES6的类(class),并说明它与传统构造函数的区别。
ES6的类提供了一种更简洁的面向对象编程方式,对比传统的构造函数,具有更好的可读性和可维护性。类使用`class`定义,`constructor`定义构造方法,`extends`实现继承,并可直接定义静态方法。示例展示了如何创建`Person`类、`Student`子类以及它们的方法调用。
20 2
|
23天前
|
JavaScript 前端开发
js开发:请解释什么是ES6的async/await,以及它如何解决回调地狱问题。
ES6的`async/await`是基于Promise的异步编程工具,能以同步风格编写异步代码,提高代码可读性。它缓解了回调地狱问题,通过将异步操作封装为Promise,避免回调嵌套。错误处理更直观,类似同步的try...catch。
|
23天前
|
JavaScript
js开发:请解释什么是ES6的Generator函数,以及它的用途。
ES6的Generator函数是暂停恢复的特殊函数,用yield返回多个值,适用于异步编程和流处理,解决了回调地狱问题。
16 6
|
23天前
|
存储 JavaScript 索引
js开发:请解释什么是ES6的Map和Set,以及它们与普通对象和数组的区别。
ES6引入了Map和Set数据结构。Map的键可为任意类型,有序且支持get、set、has、delete操作;Set存储唯一值,提供add、delete、has方法。两者皆可迭代。示例展示了Map和Set的基本用法,如添加、查询、删除元素。
13 2
|
23天前
|
JavaScript
js开发:请解释什么是ES6的Symbol,以及它的用途。
ES6的Symbol数据类型创建唯一值,常用于对象属性键(防冲突)和私有属性。示例展示了如何创建及使用Symbol:即使描述相同,两个Symbol也不等;作为对象属性如`obj[symbol1] = &#39;value1&#39;`;也可作枚举值,如`Color.RED = Symbol(&#39;red&#39;)`。
|
23天前
|
JavaScript
js开发:请解释什么是ES6的扩展运算符(spread operator),并给出一个示例。
ES6的扩展运算符(...)用于可迭代对象展开,如数组和对象。在数组中,它能将一个数组的元素合并到另一个数组。例如:`[1, 2, 3, 4, 5]`。在对象中,它用于复制并合并属性,如`{a: 1, b: 2, c: 3}`。
12 3
|
23天前
|
JavaScript
js开发:请解释什么是ES6的默认参数(default parameters),并给出一个示例。
ES6允许在函数参数中设置默认值,如`function greet(name = &#39;World&#39;) {...}`。当调用函数不传入`name`参数时,它将默认为&#39;World&#39;,提升代码简洁性和可读性。例如:`greet()`输出&quot;Hello, World!&quot;,`greet(&#39;Alice&#39;)`输出&quot;Hello, Alice!&quot;。
14 4
|
23天前
|
JavaScript 前端开发
js开发:请解释什么是ES6的解构赋值(destructuring assignment),并给出一个示例。
ES6的解构赋值简化了JavaScript中从数组和对象提取数据的过程。例如,`[a, b, c] = [1, 2, 3]`将数组元素赋值给变量,`{name, age} = {name: &#39;张三&#39;, age: 18}`则将对象属性赋值给对应变量,提高了代码的可读性和效率。
|
24天前
|
SQL JavaScript
js开发:请解释什么是ES6的模板字符串(template string),并给出一个示例。
ES6的模板字符串以反引号包围,支持变量和表达式插入以及多行书写。例如,插入变量值`Hello, ${name}!`,计算表达式`${num1 + num2}`,以及创建多行字符串。模板字符串保留原始空格和缩进,简化了字符串拼接,提高了代码可读性。
18 6