转载请注明出处: Generator函数语法解析

Generator函数是ES6提供的一种异步编程解决方案,语法与传统函数完全不同。以下会介绍一下Generator函数。

写下这篇文章的目的其实很简单,是想梳理一下自己对于Generator的理解,同时呢,为学习async函数做一下知识储备。


Generator函数

  1. 基本概念
  2. yield表达式
  3. next方法
  4. next方法的参数
  5. yield*表达式
  6. 与Iterator接口的关系
  7. for...of循环
  8. 作为对象属性的Generator函数
  9. Generator函数中的this
  10. 应用

基本概念

对于Generator函数(也可以叫做生成器函数)的理解,可以从四个方面:

形式上:Generator函数是一个普通的函数,不过相对于普通函数多出了两个特征。一是在function关键字和函数明之间多了'*'号;二是函数内部使用了yield表达式,用于定义Generator函数中的每个状态。

语法上:Generator函数封装了多个内部状态(通过yield表达式定义内部状态)。执行Generator函数时会返回一个遍历器对象(Iterator对象)。也就是说,Generator是遍历器对象生成函数,函数内部封装了多个状态。通过返回的Iterator对象,可以依次遍历(调用next方法)Generator函数的每个内部状态。

调用上:普通函数在调用之后会立即执行,而Generator函数调用之后不会立即执行,而是会返回遍历器对象(Iterator对象)。通过Iterator对象的next方法来遍历内部yield表达式定义的每一个状态。

写法上:*号放在哪里好像都可以也。看个人习惯吧,我喜欢第一种写法

function *gen () {}   √
function* gen () {}
function * gen () {}
function*gen () {}

yield表达式

yield,英文意思即产生、退让的意思,因此yield表达式也有两种作用:定义内部状态暂停执行

举一个栗子吧: )

function *gen () {
yield 1
yield 2
return 3
} const g = gen() // Iterator对象
g.next() // {value: 1, done: false}
g.next() // {value: 2, done: false}
g.next() // {value: 3, done: true}

从上面代码中可以看出,gen函数使用yield表达式定义了两个内部状态。同时呢,也可以看出来,return语句只能有一个,而yield表达式却可以有多个。

执行gen函数之后,会返回一个遍历器对象,而不是立即执行gen函数。如果需要获取yield表达式定义的每个状态,需要调用next方法。

每调用一次next方法都会返回一个包含value和done属性的对象,此时会停留在某个yield表达式结尾处。value属性值即是yield表达式的值;done属性是布尔值,表示是否遍历完毕。

另外呢,yield表达式没有返回值,或者说返回值是undefined。待会会说明一下如何给yield表达式传递返回值。

需要注意的是,yield表达式的值,只有调用next方法时才能获取到。因此等于为JavaScript提供了手动的'惰性求值'(Lazy Evaluation)的功能。

一般情况下,Generator函数会结合yield表达式使用,通过yield表达式定义多个内部状态。但是,如果不使用yield表达式的Generator函数就成为了一个单纯的暂缓执行函数,个人感觉没什么意义...

function *gen () {
console.log('凯斯')
} window.setTimeout(() => {
gen().next()
}, 2000) // 不使用yield表达式来暂停函数的执行,还不如使用普通函数呢..
// 所以Generator函数配合yield表达式使用效果更佳

另外,yield表达式如果用在另一个表达式中,需要为其加上圆括号。作为函数参数和语句是可以不使用圆括号。

function *gen () {
console.log('hello' + yield) ×
console.log('hello' + (yield)) √
console.log('hello' + yield '凯斯') ×
console.log('hello' + (yield '凯斯')) √
foo(yield 1) √
const param = yield 2 √
}

next方法

yield表达式具有暂停执行的功能,而恢复执行的是next方法。每一次调用next方法,就会从函数头部或者上一次停下来的地方开始执行,直到遇到下一个yield表达式(return 语句)为止。同时,调用next方法时,会返回包含value和done属性的对象,value属性值可以为yield表达式、return语句后面的值或者undefined值,done属性表示遍历是否结束。

遍历器对象的next方法(从Generator函数继承而来)的运行逻辑如下

  1. 遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield表达式后面的那个表达式的值,作为返回的对象的value属性值。
  2. 下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
  3. 如果没有再遇到新的yield表达式,就一直运行到函数结束,直到遇到return语句为止,并将return语句后面表达式的值,作为返回的对象的value属性值。
  4. 如果该函数没有return语句,则返回的对象的value属性值为undefined。

从上面的运行逻辑可以看出,返回的对象的value属性值有三种结果:

  1. yield表达式后面的值
  2. return语句后面的值
  3. undefined

也就是说,如果有yield表达式,则value属性值就是yield表达式后面的指;如果没有yield表达式,value属性值就等于return语句后面的值;如果yield表达式和return语句都不存在的话,则value属性值就等于undefined。举个例子: )

function *gen () {
yield 1
yield 2
return 3
} const g = gen()
g.next() // {value: 1, done: false}
g.next() // {value: 2, done: false}
g.next() // {value: 3, done: true}
g.next() // {value: undefined, done: true}

根据next运行逻辑再针对这个例子,就很容易理解了。调用gen函数,返回遍历器对象。

第一次调用next方法时,在遇到第一个yield表达式时停止执行,value属性值为1,即yield表达式后面的值,done为false表示遍历没有结束;

第二次调用next方法时,从暂停的yield表达式后开始执行,直到遇到下一个yield表达式后暂停执行,value属性值为2,done为false;

第三次调用next方法时,从上一次暂停的yield表达式后开始执行,由于后面没有yield表达式了,所以遇到return语句时函数执行结束,value属性值为return语句后面的值,done属性值为true表示已经遍历完毕了。

第四次调用next方法时,value属性值就是undefined了,此时done属性为true表示遍历完毕。以后再调用next方法都会是这两个值。

next方法的参数

yield表达式本身没有返回值,或者说总是返回undefined。

function *gen () {
var x = yield 'hello world'
var y = x / 2
return [x, y]
}
const g = gen()
g.next() // {value: 'hello world', done: false}
g.next() // {value: [undefined, NaN], done: true}

从上面代码可以看出,第一次调用next方法时,value属性值是'hello world',第二次调用时,由于变量y的值依赖于变量x,而yield表达式没有返回值,所以返回了undefined给变量x,此时undefined / 2为NaN。

要解决上面的问题,可以给next方法传递参数。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。

function *gen () {
var x = yield 'hello world'
var y = x / 2
return [x, y]
}
const g = gen()
g.next() // {value: 'hello world', done: false}
g.next(10) // {value: [10, 5], done: true}

当给第二个next方法传递参数10时,yield表达式的返回值为10,即var x = 10,所以此时变量y为5。

注意,由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的。V8引擎直接忽略第一次使用next方法的参数,只有从第二次使用next方法开始,参数才是有效的。从语义上说,第一个next方法用来启动遍历器对象,所以不用带上参数。所以呢,每次使用next方法会比yield表达式要多一次。

如果想要第一次调用next方法时就可以传递参数,可以使用闭包的方式。

// 实际上就是在闭包内部执行了一次next方法
function wrapper (gen) {
return function (...args) {
const genObj = gen(...args)
genObj.next()
return genObj
}
}
const generator = wrapper(function *generator () {
console.log(`hello ${yield}`)
return 'done'
})
const a = generator().next('keith')
console.log(a) // hello keith, done

实际上,yield表达式和next方法构成了双向信息传递。yield表达式可以向外传递value值,而next方法参数可以向内传递值。

yield*表达式

如果在Generator函数中调用另一个Generator函数,默认情况下是无效的。

function *foo () {
yield 1
}
function *gen () {
foo()
yield 2
}
const g = gen()
g.next() // {value: 2, done: false}
g.next() // {value: undefined, done: true}

从上面代码中可以看出,并没有在yield 1处停止执行。此时就需要使用yield* 表达式。从语法角度上说,如果yield表达式后面跟着遍历器对象,需要在yield表达式后面加上星号,表明它返回的是一个遍历器对象。实际上,yield*表达式是for...of循环的简写,完全可以使用for...of循环来代替yield*表达式

function *foo () {
yield 1
}
function *gen () {
yield* foo()
yield 2
}
const g = gen()
g.next() // {value: 1, done: false}
g.next() // {value: 2, done: false}
g.next() // {value: undefined, done: true} // 相当于
function *gen () {
yield 1
yield 2
} // 相当于
function *gen () {
for (let item of foo()) {
yield item
}
yield 2
}

如果直接使用了yield foo(),返回的对象的value属性值为一个遍历器对象。而不是Generator函数的内部状态。

function *foo () {
yield 1
}
function *gen () {
yield foo()
yield 2
}
const g = gen()
g.next() // {value: Generator, done: false}
g.next() // {value: 2, done: false}
g.next() // {value: undefined, done: true}

另外,任何数据类型(Array, String)只要有Iterator接口,就能够被yield*遍历

const arr = ['a', 'b']
const str = 'keith'
function *gen () {
yield arr
yield* arr
yield str
yield* str
}
const g = gen()
g.next() // {value: ['a', 'b'], done: false}
g.next() // {value: 'a', done: false}
g.next() // {value: 'b', done: false}
g.next() // {value: 'keith', done: false}
g.next() // {value: 'k', done: false}
...

如果在Generator函数中存在return语句,则需要使用let value = yield* iterator方式获取返回值。

function *foo () {
yield 1
return 2
}
function *gen () {
var x = yield* foo()
return x
}
const g = gen()
g.next() // {value: 1, done: false}
g.next() // {value: 2, done: true}

使用yield*表达式可以很方便的取出嵌套数组的成员。

// 普通方法
const arr = [1, [[2, 3], 4]]
const str = arr.toString().replace(/,/g, '')
for (let item of str) {
console.log(+item) // 1, 2, 3, 4
} // 使用yield*表达式
function *gen (arr) {
if (Array.isArray(arr)) {
for (let i = 0; i < arr.length; i++) {
yield * gen(arr[i])
}
} else {
yield arr
}
}
const g = gen([1, [[2, 3], 4]])
for (let item of g) {
console.log(item) // 1, 2, 3, 4
}

与Iterator接口的关系

任何一个对象的Symbol.iterator属性,指向默认的遍历器对象生成函数。而Generator函数也是遍历器对象生成函数,所以可以将Generator函数赋值给Symbol.iterator属性,这样就使对象具有了Iterator接口。默认情况下,对象是没有Iterator接口的。

具有Iterator接口的对象,就可以被扩展运算符(...)解构赋值Array.fromfor...of循环遍历了。

const person = {
name: 'keith',
height: 180
}
function *gen () {
const arr = Object.keys(this)
for (let item of arr) {
yield [item, this[item]]
}
}
person[Symbol.iterator] = gen
for (let [key, value] of person) {
console.log(key, value) // name keith , height 180
}

Generator函数函数执行之后,会返回遍历器对象。该对象本身也就有Symbol.iterator属性,执行后返回自身

function *gen () {}
const g = gen()
g[Symbol.iterator]() === g // true

for...of循环

for...of循环可以自动遍历Generator函数生成的Iterator对象,不用调用next方法。

function *gen () {
yield 1
yield 2
yield 3
return 4
}
for (let item of gen()) {
console.log(item) // 1 2 3
}

上面代码使用for...of循环,依次显示 3 个yield表达式的值。这里需要注意,一旦next方法的返回对象的done属性为true,for...of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回的6,不包括在for...of循环之中。

作为对象属性的Generator函数

如果一个对象有Generator函数,那么可以使用简写方式

let obj = {
* gen () {}
}
// 也可以完整的写法
let obj = {
gen: function *gen () {}
}

当然了,如果是在构造函数中,简写形式也是一样的。

class F {
* gen () {}
}

Generator函数中的this

Generator函数中的this对象跟构造函数中的this对象有异曲同工之处。先来看看构造函数中的new关键字的工作原理。

function F () {
this.a = 1
}
const f = new F()
  1. 调用构造函数F,返回实例对象f
  2. 将构造函数内部中的this指向这个实例对象
  3. 将构造函数中的原型对象赋值给实例对象的原型
  4. 执行构造函数中的代码

调用Generator函数会返回遍历器对象,而不是实例对象,因此无法获取到this指向的实例对象上的私有属性和方法。但是这个遍历器对象可以继承Generator函数的prototype原型对象上的属性和方法(公有属性和方法)。

function *Gen () {
yield this.a = 1
}
Gen.prototype.say = function () {
console.log('keith')
}
const g = new Gen()
g.a // undefined
g.say() // 'keith'

如果希望修复this指向性问题,可以使用call方法将函数执行时所在的作用域绑定到Generator.prototype原型对象上。这样做,会使私有属性和方法变成公有的了,因为都在原型对象上了。

function *Gen () {
this.a = 1
yield this.b = 2
yield this.c = 3
}
const g = Gen.call(Gen.prototype)
g.next() // {value: 2, done: false}
g.next() // {value: 3, done: false}
g.next() // {value: undefined, done: true}
g.a // 1,继承自Gen.prototype
g.b // 2,同上
g.c // 3,同上

应用

Generator函数的应用主要在异步编程上,会在下一篇文章中分享。请期待噢: )

Generator函数语法解析的更多相关文章

  1. Generator函数异步应用

    转载请注明出处: Generator函数异步应用 上一篇文章详细的介绍了Generator函数的语法,这篇文章来说一下如何使用Generator函数来实现异步编程. 或许用Generator函数来实现 ...

  2. Generator函数执行器-co函数库源码解析

    一.co函数是什么 co 函数库是著名程序员 TJ Holowaychuk 于2013年6月发布的一个小工具,用于 Generator 函数的自动执行.短小精悍只有短短200余行,就可以免去手动编写G ...

  3. 15.Generator 函数的语法

    Generator 函数的语法 Generator 函数的语法 简介 基本概念 Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同.本章详细介绍 Generat ...

  4. 16.Generator函数的语法

    1.简介 Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同. 执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机 ...

  5. ES6的新特性(16)——Generator 函数的语法

    Generator 函数的语法 简介 基本概念 Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同.本章详细介绍 Generator 函数的语法和 API,它的 ...

  6. Generator 函数的语法

    简介 § ⇧ 基本概念 Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同.本章详细介绍 Generator 函数的语法和 API,它的异步编程应用请看< ...

  7. Generator函数的语法

    简介 Generator函数是ES6关于异步编程的解决方案.Generator函数能够让函数暂停执行(即交出函数的执行权),简单直白点来理解,Generator函数就是一个状态机,内部封装了多个状态( ...

  8. With语句以及@contextmanager的语法解析

    with 语句以及@contextmanager的语法解析   with语句可以通过很简单的方式来替try/finally语句. with语句中EXPR部分必须是一个包含__enter__()和__e ...

  9. 转: ES6异步编程:Generator 函数的含义与用法

    转: ES6异步编程:Generator 函数的含义与用法 异步编程对 JavaScript 语言太重要.JavaScript 只有一根线程,如果没有异步编程,根本没法用,非卡死不可. 以前,异步编程 ...

随机推荐

  1. 《Office 365 开发入门指南》公开邀请试读,欢迎反馈

    终于等来了这一天,可以为我的这本新书画上一个句号.我记得是在今年的2月份从西雅图回来之后,就萌发了要为中国的Office 365开发人员写一些东西并最终能帮到更多中国用户的想法,而从2月26日正式写下 ...

  2. 【二十五】cookie与session学习总结

    一:cookie 1.创建cookie 关键字:setcookie 用于保存cookie 原理:当浏览器访问cookie.php页面时,我们的服务器就会以set-cookie的方式将cookie信息回 ...

  3. (一)最小的Django

    本文为作者原创,转载请注明出处(http://www.cnblogs.com/mar-q/)by 负赑屃 本文基本内容均出自<Lightweight Django>(中文为<轻量级D ...

  4. C#中MessageBox用法大全(附效果图)

    1.最简单的,只显示提示信息 2. 可以给消息框加上标题. 3. "确定"和"取消" 4. 给MessageBox加上一个Icon,.net提供常见的Icon共 ...

  5. Effective Java 第三版——11. 重写equals方法时同时也要重写hashcode方法

    Tips <Effective Java, Third Edition>一书英文版已经出版,这本书的第二版想必很多人都读过,号称Java四大名著之一,不过第二版2009年出版,到现在已经将 ...

  6. ORACLE的锁机制

    数据库是一个多用户使用的共享资源.当多个用户并发地存取数据时,在数据库中就会产生多个事务同时存取同一数据的情况.若对并发操作不加控制就可能会读取和存储不正确的数据,破坏数据库的一致性. 加锁是实现数据 ...

  7. h5拖拽上传图片

    h5实现拖拽上传图片 本文将为大家介绍如何通过js实现拖拽上传图片. 首先我们要禁用调浏览器默认的拖拽事件: window.onload = function(){ //拖离 document.add ...

  8. shell的含义

    shell:壳,是操作linux最直接的方式,通过shell中输入命令和linux系统进行交互. shell是一个小盒子,每一个有独立的命名空间,登录后的操作就是一个shell(有可能是bash,zs ...

  9. Lambda 表达式,Java中应用Lambda 表达式

    一.Lambda 表达式 简单来说,编程中提到的 lambda 表达式,通常是在需要一个函数,但是又不想费神去命名一个函数的场合下使用,也就是指匿名函数. 链接:知乎 先举一个普通的 Python 例 ...

  10. Koa2和相关资料

    koa2是什么我就不介绍,这里只是收集一些有用的资料,koa这里默认就指koa2了额. koa介绍 koa(GitHub) koa(npm) 文档 Usage Guide Error Handling ...