听说你还没听过 ES6 的 Generator 函数?

138 阅读19分钟

深入解析 ES6 的 Generator,从基本语法到高级应用,探索其在异步操作、状态管理和迭代控制中的强大功能和实用技巧,助力你高效编程!

Generator,生成器函数,ES6中最难理解的语法,没有之一。Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。

  • 语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
  • 形式上,Generator 函数是一个普通函数,但是有两个特征 :
    • function关键字与函数名之间有一个星号*
    • 函数体内部使用yield表达式,定义不同的内部状态yield在英语里的意思就是“产出”)。

语法

只要给一个函数关键字后面添加一个星号*,那么这个函数就被称之为生成器generator函数

调用

调用生成器函数,不会立即执行函数体,而是会返回一个Iterator迭代器对象,调用next() 方法这继续往后执行,碰到yield关键字就暂停

next只是给yield下指令,有了next就执行一个yieldyield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的 “惰性求值” (Lazy Evaluation)的语法功能。

  • value:表示每次执行时yield后面的内容(每次调用都会从上一次停止的地方继续运行
  • done:表示当前函数状态,是否执行完
function* generator (){
  yield 'a'
  yield 'b'
  yield 'c'
  return 'ending'
}

let g = generator() // 并不是执行,相当于new了一个实例对象,这就是*作用
console.log(g.next());  // { value: 'a', done: false }
console.log(g.next());  // { value: 'b', done: false }
console.log(g.next());  // { value: 'c', done: false }
console.log(g.next());  // { value: 'ending', done: true }
console.log(g.next());  // { value: undefined, done: true }
function* foo () {
	var o = 1
    yield o++ 
    yield o++
    yield o++
}
let gen = foo() 
console.log(gen.next());  // { value: 1, done: false }
let gener = foo()
console.log(gener.next()); // { value: 1, done: false }

yield没有返回值:undefined

function* g() {
    let a = 1
    let b = yield a++ 
    console.log(b); 
    let c = yield a++
}

let gen = g()
console.log(gen.next()); 
console.log(gen.next()); 
// 输出如下:
// { value: 1, done: false }  a
// undefined                  b
// { value: 2, done: false }  a


调用的是异步代码(先执行同步代码再执行异步代码):

  1. 分步调用next() :
    由运行结果可知:前四次调用中,返回 {value: Promise, done: false} 是同步的先执行,而setTimeout中的代码(打印)是异步,等同步代码执行完后再执行。而第五次调用时,返回 {value: undefined, done: false} 和调用B() 打印b都是同步代码,故会按顺序执行
function A(num) {
    return new Promise((resolve, reject)=>{
        setTimeout(()=>{
            console.log(`异步的a${num}`)
            resolve()
        },0)
    })
}
function B() {
    console.log('同步的b')
}
function* gen() {
    yield A(1)
    yield A(2)
    yield A(3)
    yield A(4)
    yield B()
}
let g = gen()
g.next() // 第一次调用
// {value: Promise, done: false}  异步的a1
g.next() // 第二次调用
// {value: Promise, done: false}  异步的a2
g.next() // 第三次调用
// {value: Promise, done: false}  异步的a3
g.next() // 第四次调用
// {value: Promise, done: false}  异步的a4
g.next() // 第五次调用
// 同步的b   {value: undefined, done: false}  
g.next() // 第六次调用
// {value: undefined, done: true}  
  1. 一次性调用next() :
function A(num) {
    return new Promise((resolve, reject)=>{
        setTimeout(()=>{
            console.log(`异步的a${num}`)
            resolve()
        },0)
    })
}
function B() {
    console.log('同步的b')
}
function* gen() {
    yield A(1)
    yield A(2)
    yield A(3)
    yield A(4)
    yield B()
}
let g = gen()
g.next()
g.next()
g.next()
g.next()
g.next()

执行结果:

分析:前四个yield表达式后面都是setTimeout异步代码,会按顺序加入任务队列;执行到第5个yield ,后面是同步代码,会打印出同步的b ,之后返回 {value: undefined, done: false} ,同步任务完成后再从任务队列中依次执行刚刚的异步任务。 注意:如果一次性调用的是6次g.next() ,则打印完同步的b后返回的是 {value: undefined, done: true}

遍历

因为执行 Generator 函数会返回一个遍历器对象,所以可以对其进行遍历操作:返回所有的value ,最后return的值不会被返回,因为for of遇到donetrue就停止遍历了。

for-of 循环只会遍历迭代器 done 的值为 false 的结果

function* generator (){
  yield 'a'
  yield 'b'
  yield 'c'
  return 'ending'
}

for(let item of generator()){
    console.log(item)
}
// 输出:
// a
// b
// c

与 Iterator 接口的关系

任意一个对象的Symbol.iterator方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口。

var myIterable = {};
myIterable[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  yield 3;
};
[...myIterable] // [1, 2, 3]

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

function* gen(){
  // some code}
var g = gen();
g[Symbol.iterator]() === g  // true

next()方法传参

next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值,也就是替换掉上一个整个yield表达式

function* g() {
    let a = 1
    let b = yield a++ 
    console.log(b); 
    let c = yield a++
    console.log(a);
    console.log(c);
}

let gen = g()
console.log(gen.next()); 
console.log(gen.next(3)); // next的参数,用于指定被我触发的yield的执行结果
console.log(gen.next(2));
// 输出如下:
// { value: 1, done: false }
// 3
// { value: 2, done: false }
// 3
// 2
// { value: undefined, done: true }

Generator.prototype.throw()

Generator 函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获

var g = function* () {
  try {
    yield;
  } catch (e) {
    console.log('内部捕获', e);
  }
};
var i = g();
i.next();
try {
  i.throw('a');
  i.throw('b');
} catch (e) {
  console.log('外部捕获', e);
}
// 内部捕获 a
// 外部捕获 b

上面代码中,遍历器对象i连续抛出两个错误:

  • 第一个错误被 Generator 函数体内的catch语句捕获。
  • 第二次抛出错误,由于 Generator 函数内部的catch语句已经执行过了,不会再捕捉到这个错误了,所以这个错误就被抛出了 Generator 函数体,被函数体外的catch语句捕获。

throw方法可以接受一个参数,该参数会被catch语句接收,建议抛出Error对象的实例

不要混淆遍历器对象的throw方法和全局的throw命令。throw命令与g.throw方法是无关的,两者互不影响。 上面代码的错误,是用遍历器对象的throw方法抛出的,而不是用throw命令抛出的。全局的throw命令只能被函数体外的catch语句捕获

function* gen() {
  try {
    yield 1;
  } catch (e) {
    console.log('内部捕获');
  }
}
var g = gen();
g.throw(1);// Uncaught 1

上面代码中,g.throw(1) 执行时, next方法一次都没有执行过。这时,抛出的错误不会被内部捕获,而是直接在外部抛出,导致程序出错。

这种行为其实很好理解,因为第一次执行next方法,等同于启动执行 Generator 函数的内部代码,否则 Generator 函数还没有开始执行,这时throw方法抛错只可能抛出在函数外部。throw方法被捕获以后,会附带执行下一条yield表达式。也就是说,会附带执行一次next方法。


一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了 。如果此后还调用next方法,将 返回一个value属性等于undefined、done属性等于true的对象 ,即 JavaScript 引擎认为这个 Generator 已经运行结束了。

如下面的例子:

function* g() {
  yield 1;
  console.log('throwing an exception');
  throw new Error('generator broke!');
  yield 2;
  yield 3;
}
function log(generator) {
  var v;
  console.log('starting generator');
  try {
    v = generator.next();
    console.log('第一次运行next方法', v);
  } catch (err) {
    console.log('捕捉错误', v);
  }
  try {
    v = generator.next();
    console.log('第二次运行next方法', v);
  } catch (err) {
    console.log('捕捉错误', v);
  }
  try {
    v = generator.next();
    console.log('第三次运行next方法', v);
  } catch (err) {
    console.log('捕捉错误', v);
  }
  console.log('caller done');
}
log(g());
// starting generator
// 第一次运行next方法 { value: 1, done: false }
// throwing an exception
// 捕捉错误 { value: 1, done: false }
// 第三次运行next方法 { value: undefined, done: true }
// caller done

上面代码一共三次运行next方法,第二次运行的时候会抛出错误,然后第三次运行的时候,Generator 函数就已经结束了,不再执行下去了

Generator.prototype.return()

Generator 函数返回的遍历器对象,还有一个return方法,可以返回给定的值,并且终结遍历 Generator 函数

function* gen() {
  yield 1;
  yield 2;
  yield 3;
}
var g = gen();
g.next()        // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next()        // { value: undefined, done: true }
// 遍历器对象g调用return方法后,返回值的value属性就是return方法的参数foo。
// 并且,Generator 函数的遍历就终止了,返回值的done属性为true,以后再调用next方法,
// done属性总是返回true。如果return方法调用时,不提供参数,则返回值的value属性为undefined。

果 Generator 函数内部有try...finally代码块,且正在执行try代码块,那么return方法会导致立刻进入finally代码块, finally执行完以后,再次调用next() 方法会刚刚return的参数,如果没传参就返回undefined ,至此整个函数才会结束

function* numbers () {
  yield 1;
  try {
    yield 2;
    yield 3;
  } finally {
    yield 4;
    yield 5;
  }
  yield 6;
}
var g = numbers();
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.return(7) // { value: 4, done: false }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }
// 上面代码中,调用return()方法后,就开始执行finally代码块,不执行try里面剩下的代码了,
//   然后等到finally代码块执行完,再返回return()方法指定的返回值。

next()、throw()、return() 的共同点

next()throw()return()这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是 让 Generator 函数恢复执行,并且使用不同的语句替换yield表达式 next() 是将yield表达式替换成一个值

const g = function* (x, y) {
  let result = yield x + y;
  return result;
};
const gen = g(1, 2);
gen.next(); 
// Object {value: 3, done: false}
gen.next(1); 
// Object {value: 1, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = 1;

上面代码中,第二个next(1)方法就相当于将yield表达式替换成一个值1。如果next方法没有参数,就相当于替换成undefined

gen.throw(new Error('出错了')); // Uncaught Error: 出错了
// 相当于将 let result = yield x + y
// 替换成 let result = throw(new Error('出错了'));

throw() 是将yield表达式替换成一个throw语句

gen.return(2); // Object {value: 2, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = return 2;

return() 是将yield表达式替换成一个return语句。

yield* 表达式

如果在 Generator 函数内部,调用另一个 Generator 函数。需要在前者的函数体内部,自己手动完成遍历。但如果有多个 Generator 函数嵌套,写起来就非常麻烦。ES6 提供了yield* 表达式,作为解决办法,用来在一个 Generator 函数里面执行另一个 Generator 函数

function g1(){
  yield 2
  yield 3
  yield 4
}

function g2(){
  yield 1
  yield* g1()
  yield 5
}

// -----------------------------------------
// 等同于:
function* g2(){
  yield 1
  yield 2
  yield 3
  yield 4
  yield 5
}
// -----------------------------------------
// 也等同于:
function* g2(){
  yield 1
  for(let i of g1()){
    yield i
  }
  yield 5
}

作为对象属性的 Generator 函数

如果一个对象的属性是 Generator 函数,可以简写。

let obj = {
  myGeneratorMethod: function* () {
    // ···  }
};
let obj = {
  * myGeneratorMethod() {
    ···
  }
};

Generator 函数的this

Generator 函数总是返回一个遍历器,ES6 规定这个遍历器是 Generator 函数的实例,也继承了 Generator 函数的prototype对象上的方法

function* g() {}
g.prototype.hello = function () {
  return 'hi!';
};
let obj = g();
obj instanceof g // true
obj.hello() // 'hi!'
  1. 如果把g当作普通的构造函数,并不会生效,因为g返回的总是遍历器对象,而不是this对象
function* g() {
  this.a = 11;
}
let obj = g();
obj.next();
obj.a // undefined
// 上面代码中,Generator 函数g在this对象上面添加了一个属性a,但是obj对象拿不到这个属性。
  1. Generator 函数也不能跟new命令一起用,会报错。
function* F() {
  yield this.x = 2;
  yield this.y = 3;
}
new F()
// TypeError: F is not a constructor
  1. 让 Generator 函数返回一个正常的对象实例,既可以用next方法,又可以获得正常的this首先,生成一个空对象,使用call方法绑定 Generator 函数内部的this。这样,构造函数调用以后,这个空对象就是 Generator 函数的实例对象了。
    下面代码中,首先是F内部的this对象绑定obj对象,然后调用它,返回一个 Iterator 对象。这个对象执行三次next方法(因为F内部有两个yield表达式),完成 F 内部所有代码的运行。这时,所有内部属性都绑定在obj对象上了,因此obj对象也就成了F的实例。
function* F() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}
var obj = {};
var f = F.call(obj);

f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}

obj.a // 1
obj.b // 2
obj.c // 3	
  1. 执行的是遍历器对象f,但是生成的对象实例是obj,将这两个对象统一呢:一个办法就是将obj换成F.prototype
function* F() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}
var f = F.call(F.prototype);

f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}

f.a // 1
f.b // 2
f.c // 3

再将F改成构造函数,就可以对它执行new命令了。

function* gen() {
  this.a = 1;
  yield this.b = 2;
  yield this.c = 3;
}

function F() {
  return gen.call(gen.prototype);
}

var f = new F();

f.next();  // Object {value: 2, done: false}
f.next();  // Object {value: 3, done: false}
f.next();  // Object {value: undefined, done: true}

f.a // 1
f.b // 2
f.c // 3

Generator 与状态机

Generator 是实现状态机的最佳结构。

比如,下面的clock函数就是一个状态机:

let ticking = true;
let clock = function() {
  if (ticking)
    console.log('Tick!');
  else
    console.log('Tock!');
  ticking = !ticking;
}

clock函数一共有两种状态(TickTock),每运行一次,就改变一次状态。这个函数如果用 Generator 实现:

let clock = function* () {
  while (true) {
    console.log('Tick!');
    yield;
    console.log('Tock!');
    yield;
  }
};

上面的 Generator 实现与 ES5 实现对比,可以看到少了用来保存状态的外部变量ticking,这样就更简洁,更安全(状态不会被非法篡改) 、更符合函数式编程的思想,在写法上也更优雅

Generator与协程(coroutine)

  • 由于 JavaScript 是单线程语言,只能保持一个调用栈。引入协程以后,每个任务可以保持自己的调用栈。这样做的最大好处,就是抛出错误的时候,可以找到原始的调用栈。不至于像异步操作的回调函数那样,一旦出错,原始的调用栈早就结束。
  • Generator 函数是 ES6 对协程的实现,但属于不完全实现 :
    • Generator 函数被称为“半协程”(semi-coroutine),意思是只有 Generator 函数的调用者,才能将程序的执行权还给 Generator 函数。
    • 如果是完全执行的协程,任何函数都可以让暂停的协程继续执行。
    • 如果将 Generator 函数当作协程,完全可以将多个需要互相协作的任务写成 Generator 函数,它们之间使用yield表达式交换控制权。

Generator与上下文

  • JavaScript 代码运行时,会产生一个全局的上下文环境(context,又称运行环境),包含了当前所有的变量和对象。
  • 然后,执行函数(或块级代码)的时候,又会在当前上下文环境的上层,产生一个函数运行的上下文,变成当前(active)的上下文,由此形成一个上下文环境的堆栈(context stack)。
  • 这个堆栈是“后进先出”的数据结构,最后产生的上下文环境首先执行完成,退出堆栈,然后再执行完成它下层的上下文,直至所有代码执行完成,堆栈清空。

Generator 函数不是这样,它执行产生的上下文环境一旦遇到yield命令,就会暂时退出堆栈但是并不消失,里面的所有变量和对象会冻结在当前状态等到对它执行next命令时,这个上下文环境又会重新加入调用栈冻结的变量和对象恢复执行

function* gen() {
  yield 1;
  return 2;
}
let g = gen();
console.log(
  g.next().value,
  g.next().value,
);

上面代码中,第一次执行g.next()时,Generator 函数gen的上下文会加入堆栈,即开始运行gen内部的代码。等遇到yield 1时,gen上下文退出堆栈,内部状态冻结。第二次执行g.next()时,gen上下文重新加入堆栈,变成当前的上下文,重新恢复执行。

自动执行Generator函数

我们可以借助 Promise + Generator 可以手动控制异步代码和同步代码的执行顺序了,可是这么写目前依然存在一个很严峻的问题——类似于“回掉地狱”,代码层次不清晰等。

g.next().value.then(() => {
  g.next().value.then(() => {
    g.next()
    // ......
  })
})

Thunk函数

Thunk 函数是一种用于延迟执行的函数包装器,通常用于处理异步操作或生成器函数的执行。Thunk 函数的目标是将一个函数的执行推迟到稍后的时间,以便在需要时再执行。在 JavaScript 中,Thunk 函数通常是一个带有回调函数的函数包装器。

如下例子:A函数执行完毕,递归进去执行 B函数,B函数执行完毕,递归进去执行 C函数。 这样一来,我们就通过打造一个 Thunk 函数和一个 Thunk 函数执行器(run)来实现了让 Generator 函数自动执行下一次层的 next()

// 异步函数 A、B 和 C,现在它们接受一个回调函数作为参数,并在异步操作完成后调用回调函数。
function A(callback){
  setTimeout(() => {
    console.log('异步a');
    callback();
  }, 1000);
}

function B(callback){
  setTimeout(() => {
    console.log('异步b');
    callback();
  }, 500);
}

function C(){
  setTimeout(() => {
    console.log('异步c');
    callback()
  }, 100);
}

// Thunk函数:
function SimpleThunk(fn){
  return function(callback){
    fn(function (result){
      callback(result)
    })
  }
}

// 使用Thunk函数自动执行生成器函数
function run(generator){
  const g = generator()

  function iterator(g){
    // iterate(g); 执行了第一个 g.next(),这就开启了 yeild simpleThunk(A); 的执行,
    // 得到的仍然是一个函数体,所以 const { value, done } = g.next(); 中value还是一个函数
    const {value, done} = g.next()

    if(done){
      return
    }

    if(typeof value === 'function'){
      // 调用 value() 带来了 函数A 的执行,A在1秒钟之后才会调用自己内部的 callback,
      // 这就导致了 A函数没有执行完毕的话,iterate(g) 这一处的递归就无法开始
      value(()=>iterator(g))
    } else {
      throw new Error('Generator函数应该返回一个Thunk函数')
    }
  }

  iterator(g)
}

// 生成器函数
function* generator(){
  yield SimpleThunk(A)
  yield SimpleThunk(B)
  yield SimpleThunk(C)
}

// 使用run函数自动执行生成器函数
run(generator)

// 输出:
// 异步a
// 异步b
// 异步c

co模块

除了 Thunk 函数的方式,还有一个方法可以实现 Generator 的自动执行,就是 co 模块

co 模块是大佬 TJ Holowaychuk封装的一个库,一个用于控制生成器函数执行的库,它允许你以同步的方式编写异步代码,使得生成器函数内部的异步操作看起来像同步代码一样。

co 模块的实现基于 Promise 和生成器函数的特性,它自动迭代生成器并处理 Promise 对象的返回值。

用法

原理

和Thunk函数一样同样是使用递归,不过 co 借助了 Promise中的 then 方法,所以需要使用者注意 yeild 后面的内容一定要返回一个 Promise 对象当上一个 yeild 执行完毕且状态变更为 fulfilledthen才能执行,也就才能走进下一层的递归

async/await 的实现就是由 Generator + Promise + co的手段 来封装的

Thunk和co的区别

co 和 Thunk 函数都有各自的用途和优势,选择使用哪个取决于你的具体需求和代码结构 :

  • co 更适合复杂的异步控制流程
  • 而 Thunk 函数更适合将异步操作封装成可延迟执行的函数。

当然,实际开发过程中,肯定是直接 async/await呀!

1. 用途

  • co 函数通常用于协调和管理异步操作的流程,使得异步操作看起来像同步代码一样执行。它通常与生成器函数结合使用。
  • Thunk 函数主要用于封装异步操作,将其包装成一个函数,以便在需要时延迟执行。Thunk 函数通常用于构建异步流程控制工具。

2. 执行方式

  • co 函数是一个库或工具,它使用 Promise 和生成器函数的协作来实现异步控制。co 内部会自动执行生成器函数,并管理异步操作的执行流程。
  • Thunk 函数是一个函数包装器,它接受回调函数作为参数,并通常需要手动调用来执行。Thunk 函数的执行需要显式地调用,而不像 co 那样自动进行异步流程控制。

3. 使用场景

  • co 函数适用于较复杂的异步流程控制,例如需要按顺序执行多个异步操作、处理错误等情况。它在管理多个异步任务时非常有用
  • Thunk 函数通常用于构建异步库或处理单一异步操作的情况,它更侧重于将异步操作封装成可延迟执行的函数,以便在需要时执行。

Generator应用

异步操作的同步化表达

function* main() {
  var result = yield request("http://some.url");
  var resp = JSON.parse(result);
    console.log(resp.value);
}
function request(url) {
  makeAjaxCall(url, function(response){
    it.next(response);
  });
}
var it = main();
it.next();

上面代码的main函数,就是通过 Ajax 操作获取数据。可以看到,除了多了一个yield,它几乎与同步操作的写法完全一样。注意,makeAjaxCall函数中的next方法,必须加上response参数,因为yield表达式,本身是没有值的,总是等于undefined

function* numbers() {
  let file = new FileReader("numbers.txt");
  try {
    while(!file.eof) {
      yield parseInt(file.readLine(), 10);
    }
  } finally {
    file.close();
  }
}
let n = numbers()
// 使用yield表达式可以手动逐行读取文件。
n.next()
n.next()
n.next()
...
n.next()

控制流管理

function* longRunningTask(value1) {
  try {
    var value2 = yield step1(value1);
    var value3 = yield step2(value2);
    var value4 = yield step3(value3);
    var value5 = yield step4(value4);
    // Do something with value4
  } catch (e) {
    // Handle any error from step1 through step4
  }
}

scheduler(longRunningTask(initialValue));
function scheduler(task) {
  var taskObj = task.next(task.value); // 如果Generator函数未结束,就继续调用  
  if (!taskObj.done) {
    task.value = taskObj.value
    scheduler(task);
  }
}

作为数据结构

因为 Generator 函数可以返回一系列的值,这意味着它可以对任意表达式,提供类似数组的接口

function* doStuff() {
  yield fs.readFile.bind(null, 'hello.txt');
  yield fs.readFile.bind(null, 'world.txt');
  yield fs.readFile.bind(null, 'and-such.txt');
}

for (task of doStuff()) {
  // task是一个函数,可以像回调函数那样使用它
}

等价于:

function doStuff() {
  return [
    fs.readFile.bind(null, 'hello.txt'),
    fs.readFile.bind(null, 'world.txt'),
    fs.readFile.bind(null, 'and-such.txt')
  ];
}

状态模式的语法糖

如果我们要自己去写状态模式的代码的话,会显得比较繁琐,相当于要自己去切换上下文,而有了Generator我们就可以直接利用Generator的能力为我们进行状态切换了。

频繁切换状态 :

function* func() {
  while (true) {
    yield console.log("红灯亮起");
    yield console.log("绿灯亮起");
    yield console.log("黄灯闪烁,红灯即将亮起");
  }
}

const light = func();

function start(immediate) {
  setTimeout(() => {
    light.next();
    start();
  }, 1000);
  immediate && light.next();
}

作为迭代器的语法糖

请添加某些代码使得以下代码正常运行:

const obj = { a: 1, b: 2 } 
// 在此处添加你的代码 
const [a, b] = obj;

这个题是要obj对象补迭代器 :

obj[Symbol.iterator] = function () {
    const keys = Object.keys(this);
    let idx = 0;
    const _this = this;
    return {
      next() {
        const i = idx++;
        return {
          value: _this[keys[i]],
          done: i >= keys.length,
        };
      },
    };
  }
// 定义一个Generator,读取目标对象的
function *objIterator(obj) {
  for (const key in obj) {
    if (obj.hasOwnProperty(key)) {
      yield obj[key];
    }
  }
}

const obj = { a: 1, b: 2 };

obj[Symbol.iterator] = function() {
  return objIterator(this)
}

// 在此处添加你的代码 
const [a, b] = obj;