引入
系统学习ES6各种特性,了解背后的原理。
本章记录Generator
函数
笔记
1. Generator简介
1.1 概述
Generator
函数是ES6提供的一种异步编程解决方案,其语法行为与传统函数完全不同。
从语法上,首先可以把它理解为一个状态机,内部封装了多个状态。
此外,执行此函数会返回一个遍历器对象,所以还可以理解为一个遍历器对象生成函数,该对象遍历内部的每一个状态。
Generator
函数有两个特征:
- function命令与函数名之间有一个星号
- 函数体内部使用
yield
(产出的意思)语句定义不同的内部状态
Generator
函数的调用方法和普通函数一样,但是调用后,该函数并不会执行,返回的也不是运行结果,而是一个迭代器对象。
只有调用该迭代器对象的next方法,函数内部的指针就从函数头或上一次停下的地方开始执行,直到遇到下一条yield
语句或return
语句为止。
换言之,yield
语句是暂停标志,next
方法可以恢复执行。
function* foobar(){
console.log(1)
yield 'foo'
console.log(2)
yield 'bar'
return 'foobar'
}
let foo = foobar()
foo.next() // 输出1 得到{value: 'foo', done: false}
foo.next() // 输出2 得到{value: 'bar', done: false}
foo.next() // 得到{value: 'foobar', done: true}
注意:若没有return语句时,会像普通函数一样补充一个return undefined
此外星号的位置要在function和函数名之间,左右是否间隔空格都不会影响
1.2 yield语句
yield语句是Generator函数内的暂停标志,Generator函数遇到yield语句就暂停执行后面的操作,并将紧跟在yield语句后的表达式的值作为返回的对象的value属性。
注意:
- yield语句后的表达式是惰性求值的,即运行到该yield语句才会进行求值
- yield语句与return语句的区别在于:yield语句可以执行多次,而return只有一次
- yield语句只能用于Generator函数中,其他地方都会报错
- yield表达式如果用在另一个表达式中,必须放在圆括号内,如
let t = 'foo' + (yield)
补充:
yield语句本身并没有返回值(返回undefined)
所以let t = 'foo' + (yield 'bar')
并不一定能得到'foobar',若next中没有附带参数,得到的将会是'fooundefined'
1.3 与Iterator接口的关系
由于Generator函数就是遍历器生成函数,因此可以把该其赋值给对象的Symbol.iterator
属性,从而使得该对象具有Iterator
接口。
let obj = {
[Symbol.iterator]: function* (){
yield 1;
yield 2;
yield 3;
return 666 //return(done为true)的不会被取值
}
}
console.log([...obj]) // [1, 2, 3]
Generator函数执行后的遍历器对象的Symbol.iterator属性就是该函数,执行后返回的是对象本身
2. next方法的参数
yield语句本身没有返回值或者说返回undefined
而next方法可以带有一个参数,该参数会被当做上一个yield语句的返回值
function* foo(x){
let y = 2 * (yield (x+1))
let z = yield (y/3)
return (x+y+z)
}
let t = foo(5)
t.next() // {value: 6, done: false} 因为x+1=6
t.next(12) //{value: 8, done: false}
// 因为12会作为上一条yield语句的返回值,所以y=2*(12)=24
// 所以此处得到的是y/3=8
// let y = 2 * (yield (x+1)) 可以理解为函数运行到yield (x+1)就暂停了
// 直到运行下一个next(12)时,才把12作为yield (x+1)的值进行y的赋值运算
t.next(13) // {value: 42, done: true}
//13作为yield (y/3)的返回值,所以z = 13,之前得到y=24,所以return 5+24+13=42
注意:第一个next的参数会被忽略,所以第一个next仅仅用于启动遍历器对象。
若想要第一次调用next方法时就能输入值,可以在Generator函数外再包一层
function wrapper(func){ //包裹函数生成器
return function (...args){
let res = func(...args)
res.next() // 此处执行了第一个next
return res
}
}
let wrappedFunc = wrapper( function* (){
console.log('第一次next被执行')
console.log(yield)
})
let t = wrappedFunc()
t.next('hello')
3.for of循环
for of循环可以自动遍历Generator函数生成的对象,不需要手动调用next方法
function* foo(){
yield 1;
yield 2;
yield 3;
return 666 //return(done为true)的不会被取值
}
for (let val of foo()){
console.log(val)
}
// 输出 1 2 3
但是注意:for of循环和扩展运算符一样,不会包含done为true的返回对象的值
此外,可以使用Generator函数为原生对象添加服务于for of遍历的Iterator接口
function* objectEntries(){
let props = Object.keys(this) //注意要用this
for (let prop of props){
yield [prop, this[prop]] //返回pair的形式
}
}
let foo = {a:'foo', b:'bar', c:'foobar'}
foo[Symbol.iterator] = objectEntries
for (let [k,v] of foo){
console.log(`key:${k} val:${v}`)
}
4. Generator.prototype.throw()
Generator函数返回的遍历器对象带有throw方法,可以在函数体外抛出错误,然后在函数内使用try...catch捕获。若内部的错误被捕获,会自动执行一次next方法
let t = function* (){
try{
yield;
}catch(e){
console.log('内部捕获', e);
}
yield console.log('错误若被内部捕获则自动执行到下一条yield')
}();
t.next();
// t.throw('错误') // 但是更建议抛出错误实例
t.throw(new Error('错误'))
如果函数内部没有捕获错误,那么throw方法抛出的错误将被外部的try..catch捕获。此外,因为内部的错误没有在内部捕获,所以会中断内部的程序,导致done状态变为true。
let t = function* (){
yield 1;
yield 2;
return 3;
}();
console.log(t.next()) // {value: 1, done: false}
try{
t.throw('错误')
}catch(e){
console.log('外部捕获',e);
}
console.log(t.next()) // {value: undefined, done: true} 因为内部错误未被捕获,程序已经被中断了
而外部抛出的错误并不会影响到迭代器的状态,可以继续使用next来恢复执行。
这种函数体内外错误捕获的机制方便了对错误的处理。而且出错的代码与处理错误的代码,实现了时间和空间上的分离。
5. Generator.prototype.return()
Generator函数返回的迭代器对象还有return方法,可以返回特定的值,并终结Generator函数的遍历。
但是注意:若Generator函数内有try..finally代码块时,return方法会被推迟到finally代码块执行完再执行
let g = function* (){
yield 1;
try{
yield 2; //注意必须执行到代码块内,finally才有效
}finally{
yield 3;
}
yield 4;
}();
g.next() // {value: 1, done: false}
g.next() // {value: 2, done: false}
g.return(6) // {value: 3, done: false}
g.next() // {value: 6, done: true} finally已结束,得到了之前return的6
6. yield*语句
如果在Generator函数内调用另一个Generator函数,默认情况下是没有效果的。
function* foo(){
yield 1;
yield 2;
}
function* bar(){
foo() //在bar函数看来,得到的只是一个迭代器,并不会因此而暂停
yield 3;
yield 4;
}
for (let v of bar()){
console.log(v)
}
// 3 4
使用yield*
语句,可以实现在Generator函数内执行另一个Generator函数(也是一种递归思想)
function* foo(){
yield 1;
yield 2;
}
function* bar(){
yield* foo() //yield*相当于将其右侧的迭代器拆解开,还原为一个个yield
yield 3;
yield 4;
}
// 等价于
function* bar2(){
// for of循环yield
for(let v of foo()){
yield v;
}
yield 3;
yield 4;
}
for (let v of bar()){
console.log(v)
}
// 1 2 3 4
注意:若yield*
后的Generator函数若需要获得其return值,那么需要用let val = yield* iter
的形式
yield*
的效果相当于将其右侧的迭代器拆解开,还原为一个个yield。
可以看做是for of循环使用yield的语法糖。
此外,yield*
还可以用于任何实现了Iterator接口的对象。
let gen = function* (x){
yield* x
};
let t1 = gen([1,2,3])
let t2 = gen('abc')
for (let v of t1){
console.log(v)
} // 1 2 3
[...t2] // ['a','b','c']
7. 作为对象属性的Generator函数
如果一个对象的属性是Generator函数,那么可以进行简写
let obj = {
* gen1(){ //简写形式
yield 1;
},
gen2: function* (){ //完整形式
yield 2;
}
}
8. Generator函数的this
Generator函数总是返回一个迭代器,ES6规定这个迭代器是该函数的实例,会继承函数prototype上的内容
但是Generator函数又不能简单的看做一个构造函数
- 不能使用new命令
- 返回的不是普通的this对象,而是迭代器对象
所以注意和普通函数的区别
9. 简单应用
异步操作的同步化表达
function* loadUI(){
showLoading();
yield loadUIAsync();
hideLoading();
}
let loader = loadUI()
loader.next() //显示加载界面,并开始异步加载UI界面,加载完成后返回
loader.next() //隐藏加载界面
上述代码中loadUIAsync
为异步函数,本来需要await
、或者回调
的方式来保证其执行顺序,但是使用yield可以以一种同步化的表达来编写代码,代码清晰
提供类似数组的结构
function* doStuff(){
yield func1
yield func2
yield func3
}
for (task of doStuff()){
task()
}