Generator 函数

48 阅读7分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第 31 天,点击查看活动详情

start

  • 今天学习什么? ES6的 Generator 函数。

开始

Generator

定义:

阮一峰的ES6入门:Generator 函数是ES6提供的异步解决方案,执行 Generator 函数会返回一个遍历器对象。

MDN:function\* 这种声明方式 (function关键字后跟一个星号)会定义一个生成器函数 (generator function),它返回一个 Generator 对象。

写法:

Generator 函数是一个普通函数,但是有两个特征:

  1. function关键字与函数名之间有一个星号;

  2. 函数体内部使用yield表达式,定义不同的内部状态

function* myGenerator() {
  yield 'hello'
  yield 'world'
  return 'ending'
}

var obj = myGenerator()

console.log(obj)
// Object [Generator] {}

console.log(obj.next())
// { value: 'hello', done: false }

console.log(obj.next())
// { value: 'world', done: false }

console.log(obj.next())
// { value: 'ending', done: true }

简单总结一下上述内容:

  • Generator 意思是 “生成器”;

  • yield 意思是“产出”;

  • 生成器函数function后的 * 存放的位置,ES6没有规定;

    以下写法都是可以的

    function * foo(x, y) { ··· }
    function *foo(x, y) { ··· }
    function* foo(x, y) { ··· } // 一般偏向于这种写法
    function*foo(x, y) { ··· }
    
  • 生成器函数的返回值是一个对象,它符合 "可迭代协议" 和 "迭代器协议"。

  • 既然是迭代器,所以可以用 for of遍历它,不熟悉迭代器的建议先去学习一下迭代器(Iterator)。

yield 表达式

由于 Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。

yield执行的逻辑:

(1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。

(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。

(3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。

(4)如果该函数没有return语句,则返回的对象的value属性值为undefined

yield 后面的那个表达式的值,会惰性求值。

代码示例:

function* myGenerator() {
  console.log('开始执行')
  yield '1'

  console.log('1---2')
  yield '2'

  console.log('2---3')

  /* `yield`表达式如果用在另一个表达式之中,必须放在圆括号里面。 */
  console.log(1 + (yield '3'))

  console.log('3---4')
  yield 4 + 5

  /* 最后的 return 也会执行,没有 return 也会返回 undefined */
}

var obj = myGenerator()

console.log(obj)
// Object [Generator] {}

console.log(obj.next())
// 开始执行
// { value: '1', done: false }

console.log(obj.next())
// 1---2
// { value: '2', done: false }

console.log(obj.next())
// 2---3
// { value: '3', done: false }
// NaN

console.log(obj.next())
// 3---4
// { value: 9, done: false }

console.log(obj.next())
// { value: undefined, done: true }

其他注意事项:

  1. yield表达式只能用在 Generator 函数里面,用在其他地方都会报错。

  2. yield表达式如果用在另一个表达式之中,必须放在圆括号里面。

  3. yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号。

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

  5. next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。

next传参

function* foo() {
  var a = yield 1
  console.log('a---', a)

  var b = yield 2
  console.log('b---', b)

  var c = yield 3
  console.log('c---', c)
}

var obj = foo()

/* 1. 正常使用 */
console.log(obj.next())
// { value: 1, done: false }

/* 2. 调用 .next 不传值 */
console.log(obj.next())
// a--- undefined
// { value: 2, done: false }

/* 3. 调用 .next 传值*/
console.log(obj.next('tomato'))
// b--- tomato
// { value: 3, done: false }

for of的使用

既然 Generator 函数返回的一个对象符合 "可迭代协议" 和 "迭代器协议";所以我们可以使用 for of对这个对象进行遍历。

function* foo() {
  yield 1
  yield 2
  yield 3
  return 'hh'
}

var obj = foo()
var obj2 = foo()

/* 1. 正常的使用 next依次触发 */
console.log(obj.next())
// { value: 1, done: false }

console.log(obj.next())
// { value: 2, done: false }

console.log(obj.next())
// { value: 3, done: false }

console.log(obj.next())
// { value: 'hh', done: true }



/* 2. 使用for of触发已经迭代过的 obj */
for (const iterator of obj) {
  console.log('obj', iterator)
}

/* 使用for of触发新的 obj2 */
for (const iterator of obj2) {
  console.log('obj2', iterator)
}
// obj2 1
// obj2 2
// obj2 3

由上可得:

**注意:**一旦next方法的返回对象的done属性为truefor...of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回的6,不包括在for...of循环之中。

yield*

如果在 Generator 函数内部,调用另一个 Generator 函数。

1.直接yield foo() , 返回值是一个 Generator对象:

function* foo() {
  yield 1
  yield 2
}

function* boo() {
  yield 3
  yield foo()
  yield 4
}

for (const iterator of boo()) {
  console.log(iterator)
}
// 3
// Object [Generator] {}
// 4

/* ps: 扩展运算符就底层实现就是 `for of`, 所以 ... 就类似于依次调用了 next 方法; */

2.使用for of嵌套调用

/* 2.嵌套调用 */
function* foo() {
  yield 1
  yield 2
}

function* boo() {
  yield 3
  for (let iterator of foo()) {
    console.log(iterator)
  }
  yield 4
}

for (const iterator of boo()) {
  console.log(iterator)
}

// 3
// 1
// 2
// 4

3.利用 yield* 简化嵌套调用

/* 3.简化嵌套调用 */
function* foo() {
  yield 1
  yield 2
}

function* boo() {
  yield 3
  yield* foo()
  yield 4
}

for (const iterator of boo()) {
  console.log(iterator)
}

// 3
// 1
// 2
// 4

4.总结:

对比实例2和实例3:

  • yield*后面的 Generator 函数(没有return语句时),不过是for...of的一种简写形式,完全可以用后者替代前者。

  • 反之,在有return语句时,则需要用var value = yield* iterator的形式获取return语句的值。

5.其他

很久之前学习数组扁平化,就有借助 Generator 来解决数组扁平话,我们再回头看看这里的写法,其实也是借助 yield*效果类似 for of 去递归返回每一项。

// 1.定义一个 Generator 函数
function* flatten(array) {
  // 2. for循环遍历数组
  for (const item of array) {
    // 3.如果是数组
    if (Array.isArray(item)) {
      // 4. 用for of形式处理 flatten(item)
      yield* flatten(item)
    } else {
      // 5.如果不是数组 直接 yield
      yield item
    }
  }
}

var arr = [1, 2, [3, 4, [5, 6]]]
const flattened = [...flatten(arr)]
// [1, 2, 3, 4, 5, 6]

next()、throw()、return()

next()

function* foo() {
  var a = yield 1 
  // yield 1 相当于 '传入参数'
  console.log(a)
  yield 2
  yield 3
  return 4
}

var obj = foo()

obj.next()
obj.next('传入参数')
// 传入参数

throw()

function* foo() {
  try {
    yield 1
    // yield 1 相当于 throw xxx
  } catch (error) {
    console.log('手动捕获错误', error)
  }
  yield 2
  yield 3
  return 4
}

var obj = foo()

obj.next()
obj.throw('出错啦!!')
// 手动捕获错误 出错啦!!

return()

function* foo() {
  yield 1
  //  yield 1相当于  return xxx,后续代码就不执行了

  yield 2
  yield 3
  return 4
}

var obj = foo()

obj.next()

console.log(obj.return('直接返回'))
// { value: '直接返回', done: true }

console.log(obj.next())
//  value: undefined, done: true }

简单总结一下:

  • obj.next(xxx) 相当于 yield语句 转换成 xxx

  • obj.throw(xxx) 相当于 yield语句 转换成 throw xxx

  • obj.return() 相当于 yield语句 转换成 return xxx

Generator 函数的this

问题1: 既然 Generator 是一个函数,能使用new关键词调用吗?

function* foo() {
  yield 1
  yield 2
  return 3
}


var f = new foo()
// TypeError: foo is not a constructor

答案:不可以使用 new调用

问题2: Generator函数的返回对象原型上什么

function* foo() {
  yield 1
  yield 2
  return 3
}

foo.prototype.say = function () {
  console.log('原型上添加方法')
}

var f = foo()

f.say()
// 原型上添加方法

console.log(f instanceof foo)
// true

console.log(f.__proto__ === foo.prototype)
// true

答案:虽然没有使用 new关键词,但是返回对象的隐式原型指向的就是函数的显示原型。

问题3:原型看完了,那么this指向呢?

function* foo() {
  console.log('foo的this', this)
  this.a = 'tomato'

  yield 1
  yield this.a
  yield 2
}

var f = foo()

console.log(f.next())
// foo的this Window
// value: 1, done: false}

console.log(f.next())
// {value: 'tomato', done: false}

console.log(f.next())
// {value: 2, done: false}

console.log(f.a)
// undefined

答案:

  1. 返回的对象的无法获取到 foo中this上的属性;

  2. 通过遍历器去依次 next的时候,this指向全局;

其实和 new 还是有很大差距的

如果非要实现和 new 类似的效果的话 可以这样写:

function* foo() {
  console.log('foo的this', this)
  this.a = 'tomato'
  yield 1
  yield 2
}

function a() {
  return foo.call(foo.prototype)
}

var f = new a()

f.next()
// foo的this Object [Generator] {}
f.next()
f.next()

console.log(f.a)
// tomato

需要注意一下,需要 f.next()才会开始执行 Generator 函数中的代码。

end

  • 其实在学习过遍历器的基础上,再看 Generator,就相对来说没有那么难理解了。
  • 本文主要是学习了 Generator主要用法,
  • 后续再详细看看 ,异步编程的解决方案