1. 简介
Generator 函数是 ES6 提供的一种异步编程解决方案。它既是一个生成器,也是一个状态机,内部拥有值及相关的状态,生成器返回一个迭代器 Iterator 对象,可以通过这个迭代器,手动地遍历相关的值、状态,保证正确的执行顺序。
特征:
- function 关键字和函数之间有一个星号(*),且内部使用 yield 表达式,定义不同的内部状态;
- 调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是遍历器对象(Iterator Object);
function* gen() { yield 1; yield 2; return 3; yield 4; } let g = gen(); console.log(g.next()); // {value: 1, done: false} console.log(g.next()); // {value: 2, done: false} console.log(g.next()); // {value: 3, done: true} console.log(g.next()); // {value: undefined, done: true}
每次调用 next() 方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个 yield 表达式(或 return 语句)为止。换言之,Generator 函数是分段执行的,yield 表达式是暂停执行的标记,而 next() 方法可以恢复执行。
调用 Generator 函数,返回一个遍历器对象 (Iterator),代表 Generator 函数的内部指针。每次调用遍历器对象的 next() 方法,就会返回一个有着 value 和 done 两个属性的对象。value 属性表示当前的内部状态的值,是 yield 表达式后面那个表达式的值;done 属性是一个布尔值,表示是否遍历结束。
Generator 函数的暂停执行的效果,意味着可以把异步操作写在 yield 语句里面,等到调用 next 方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在 yield 语句下面,反正要等到调用 next() 方法时再执行。所以,Generator 函数的一个重要实际意义就是用来处理异步操作 改写回调函数。
注意: 如果 return 语句后面还有 yield 表达式,那么后面的 yield 完全不生效。
2. 与 Iterator 接口的关系
任意一个对象的 Symbol.iterator 方法,是一个遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。
Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的 Symbol.iterator 属性,从而使得该对象具有 Iterator 接口。
Iterator 的 return 的值不会被 for…of 循环到 , 也不会被扩展符遍历到
function* gen() { yield 1; yield 2; return 3; } let g = gen(); console.log([...g]); // [1, 2] var obj = {}; obj[Symbol.iterator] = function* () { yield 1; yield 2; return 3; }; for(let foo of [...obj]) { console.log(foo) // 1, 2 } console.log([...obj]); // [1, 2]
3. yield 表达式
由于 Generator 函数返回的遍历器对象,只有调用 next 方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield 表达式就是暂停标志。
迭代器对象的 next 方法的运行逻辑如下:
下一次调用 next() 方法时,再继续往下执行,直到遇到下一个 yield 表达式;
如果没有再遇到新的 yield 表达式,就一直运行到函数结束,直到 return 语句为止,并将 return 语句后面的表达式的值,作为返回的对象的 value 属性值;
如果该函数没有 return 语句,则返回的对象的 value 属性值为 undefined;
注意:
yield 语句只能用于 function* 的作用域,如果 function* 的内部还定义了其他的普通函数,则函数内部不允许使用 yield 语句;
yield 语句如果参与运算,必须用括号括起来;
function* gen1() { yield 123 + 456; // yield后面的表达式123 + 456,不会立即求值,只会在next方法将指针移到这一句时,才会求值 }; const g1 = gen1(); console.log(g1.next()); // { value: 579, done: false } function* gen2() { return function () { yield 1 // SyntaxError 语法错误 } }; 'Hello' + yield 'world'; // SyntaxError 语法错误 'Hello' + (yield 'world');
4. next() 方法的参数
- yield 表达式本身没有返回值,或者说总是返回 undefined;
- next() 方法可以带一个参数,该参数会改变上一个 yield 表达式的返回值;
function* gen(x) { const y = 2 * (yield (x + 1)); const z = yield (y / 2); return (x + y + z); } let a = gen(6); console.log(a.next()); // {value: 7, done: false} console.log(a.next()); // {value: NaN, done: false} console.log(a.next()); // {value: NaN, done: true} let b = gen(6); console.log(b.next()); // {value: 7, done: false} console.log(b.next(10)); // {value: 10, done: false} console.log(b.next(8)); // {value: 34, done: true}
第一次调用 a 的 next() 方法时,返回 x+1 的值 7。第二次运行 a 的 next() 方法的时候不带参数,导致 y 的值等于2 * undefined(即NaN),除以 3 以后还是NaN,因此返回对象的 value 属性也等于 NaN。第三次运行a的 next() 方法的时候不带参数,所以 z 等于 undefined,返回对象的 value 属性等于 5 + NaN + undefined,即NaN。第一次调用 b 的 next() 方法时,返回 x+1 的值7;第二次调用 next() 方法,将上一次 yield 表达式的值设为10,因此 y 等于20,返回 y / 2 的值 10;第三次调用 next() 方法,将上一次 yield 表达式的值设为 8,因此 z 等于 8,这时 x 等于6,y 等于20,所以 return 语句的值等于 6+20+8。
注意:
- 在第一次使用 next 方法时,传递参数是无效的;
- V8 引擎直接忽略第一次使用 next() 方法时的参数,只有从第二次使用next方法开始,参数才是有效的;
- 从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数;
5. 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 语句捕获。
i 第二次抛出错误,由于 Generator 函数内部的catch语句已经执行过了,不会再捕捉到这个错误了,所以这个错误就被抛出了 Generator 函数体,被函数体外的 catch 语句捕获。
throw 方法可以接受一个参数,该参数会被 catch 语句接收,建议抛出 Error 对象的实例。
var g = function* () { try { yield; } catch (e) { console.log(e); } }; var i = g(); i.next(); i.throw(new Error('出错了!')); // Error: 出错了!(…)
注意: 不要混淆遍历器对象的 throw 方法和全局的 throw 命令。上面代码的错误,是用遍历器对象的 throw 方法抛出的,而不是用 throw 命令抛出的。后者只能被函数体外的 catch 语句捕获。
如果 Generator 函数内部没有部署 try…catch 代码块,那么 throw 方法抛出的错误,将被外部 try…catch 代码块捕获。
var g = function* () { while (true) { yield; console.log('内部捕获', e); } }; var i = g(); i.next(); try { i.throw('a'); i.throw('b'); } catch (e) { console.log('外部捕获', e); } // 外部捕获 a
上面代码中,Generator 函数 g 内部没有部署 try…catch 代码块,所以抛出的错误直接被外部 catch 代码块捕获。
如果 Generator 函数内部和外部,都没有部署 try…catch 代码块,那么程序将报错,直接中断执行。
var gen = function* gen(){ yield console.log('hello'); yield console.log('world'); } var g = gen(); g.next(); g.throw(); // hello // Uncaught undefined
上面代码中,g.throw 抛出错误以后,没有任何 try…catch 代码块可以捕获这个错误,导致程序报错,中断执行。
throw 方法被捕获以后,会附带执行下一条 yield 表达式。也就是说,会附带执行一次 next 方法。
var gen = function* gen(){ try { yield console.log('a'); } catch (e) { // ... } yield console.log('b'); yield console.log('c'); } var g = gen(); g.next() // a g.throw() // b g.next() // c
上面代码中,g.throw 方法被捕获以后,自动执行了一次next方法,所以会打印 b。另外,也可以看到,只要 Generator 函数内部部署了 try…catch 代码块,那么遍历器的 throw 方法抛出的错误,不影响下一次遍历。
一旦 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 函数就已经结束了,不再执行下去了。
6. 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。
return 的参数值覆盖本次 yield 语句的返回值,并且提前终结遍历,即使后面还有 yield 语句也一律无视。
并且 Generator 函数的遍历就终止了,返回值的 done 属性为 true,以后再调用 next() 方法,done 属性总是返回 true。
如果 return() 方法调用时,不提供参数,则返回值的 value 属性为 undefined。