ES6学习笔记(十五)Generator函数

本文深入探讨了ES6中Generator函数的使用方法和特性,包括其基本语法、yield表达式的功能、不同方法如next、throw和return的作用,以及yield*表达式的高级应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Generator函数是ES6中提供的一种异步解决方案,该函数同时也可以用来部署遍历器。


Generator函数的使用


Generator函数和普通的函数不一样,其function和函数名之间有个*,函数体内部用yield表达式来定义不同的状态。

function* gen(){

    yield 1;

    yield 2;

    yield 3;

}

上面代码即为一个Generator函数,与普通函数一样,Generator函数也是在函数名后面加括号传入参数来调用参数的,但函数不会立即执行,会生成一个迭代器控制这个Generator函数的执行,需要使用next才会继续执行,且是执行到yield表达式后的那一句停止,等待下一个next。

function* gen(){

    yield 1;

    yield 2;

    yield 3;

}

var g=gen()

g.next() // {value: 1, done: false}

g.next() // {value: 2, done: false}

g.next() // {value: 3, done: false}

g.next() // {value: undefined, done: true}

如上面代码所示,每次调用next都会让函数往后执行,且返回一个对象,value为yield表达式后面的内容,done表示遍历是否结束,为true时就结束。

yield表达式


yield表达式是Generator函数中的暂停标志,若后面没有yield表达式,有return语句时,执行到return语句,返回value为return后面的内容的对象,没有return语句时,返回value为undefined的对象,done都为true。如果在两个yield表达式中间有return语句,则done会提前变为true。

function* gen(){
    yield 1;
    yield 2;
    return 3;
    yield 4;
}

var 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}

上面代码可以看到,因为提前return了,所以在value为3时done就为true了,不会执行到下面的yield 4。

若yield后面没有任何代码,则等同于yield undefined;

function* fn(){
    yield;
}

//等同于

function* fn(){
    yield undefined;
}

若yield表达式后面是一个表达式,那么会在next语句执行到这一句时才进行求值,即惰性求值

function* gen() {

  yield  123 + 456;

}

像上面代码中,123+456就是在执行到这一句时才求值的。

当然,Generator函数里面可以没有yield语句,此时的Generator函数就变成一个暂缓执行的函数,当调用next时就会执行整个函数,但没调用和next之前,函数是不会执行的。

function* gen(){

    console.log('no yield');

}

var g=gen()

g.next();
// {value: undefined, done: true}

//  no yield

要注意的是,yield表达式只能在Generator函数中使用,在其他地方使用会报错。

(function (){

  yield 1;

})()

// Uncaught SyntaxError: Unexpected number

即使在Generator函数中,若在该函数中再嵌套了一个非Generator函数,而yield语句又在这个函数里面,则也会报错。

function* gen() {

    let arr = [1, 2, 3];

    arr.map(function(val) {

        yield val;

    })

}

// Uncaught SyntaxError: Unexpected identifier

上面代码中,yield语句实际上是在map参数中的函数的,而该函数不是一个Generator函数,所以在声明函数的时候就报错了。

另外,yield若要用在另一个表达式中,必须放在圆括号内,否则会报错。

function* gen(){
    console.log(2+yield 3 );
}

// Uncaught SyntaxError: Unexpected identifier

function* gen(){
    console.log(2+(yield 3));
}

// 不报错

实际上,关于yield的位置,可以参考赋值表达式的位置,使用a=3这个例子来看,当某个位置可以使用a=3时,就可以直接写上yield表达式,如果不行,可以将yield表达式写在括号内。如下代码:

var a, b;

a = 3; // 合法
b = 2 + a = 3; // 不合法
b = 2 + (a = 3); // 合法

yield 3; // 合法
a = 2 + yield 3; // 不合法
a = 2 + (yield 3); // 合法

此外,yield和=运算符一样是右结合的,看如下代码

yield yield yield 3;

//等同于

yield( yield ( yield 3) );

Generator的方法


Generator函数返回一个遍历器,该遍历器有三个方法使函数走出“冻结”的状态。

next方法

该方法使Generator函数执行到下一条yield语句,若之后没有yield语句,则执行到函数尾部,第一个次调用next时算启用Genertor函数。

该方法返回一个有value属性和done属性的对象,value属性的值为yield表达式后的内容,done表示遍历是否已经结束。

function* gen(){
    yield 1;
    yield 2;
    return 3;
}

var 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方法的参数是yield表达式返回的值,第一个调用的next方法等同于启用Generator函数,所以并不会把参数给任一个yield表达式作为返回值,而后面的next方法就一次给yield表达式返回值。

function *gen(){
    var a=yield 2;
    var b=yield 3;
    console.log(a,b);
}
var it=gen();
it.next();
it.next(1);
it.next(2);
it.next(3);

// 1  2

如上代码,第二个next方法的参数1作为第一个yield表达式yield 2的返回值赋值给变量a,而第三个next方法的参数2作为第二个yield表达式yield 3的返回值赋值给变量b,所以最后打印出来的结果是1  2。

throw方法

该方法会在Generator函数体外抛出一个错误,在函数体内处理。

function* gen(){
    try{
        yield;
    }catch(e){
        console.log(`Generator内部  ${e}`);
    }
}

var g=gen();

g.next();

try{
    g.throw('error');
}catch(e){
    console.log(`Generator 外部  ${e}`)
}

// Generator内部  error

上面代码可以看到,最后throw抛出的错误被Generator函数内部的catch捕获了。同时也可以看到,throw方法的参数被catch获取到了,若没有传入参数,catch获取的是undefined。

若Generator方法内部没有try catch语句,那么错误要在外部捕获

function* gen(){
    yield;
}

var g=gen();

g.next();

try{
    g.throw('error');
}catch(e){
    console.log(`Generator 外部  ${e}`)
}

// Generator 外部  error

若内部和外部都没有部署try...catch语句,那么将会报错。

function* gen(){
    yield;
}

var g=gen();

g.next();

g.throw('error');

// Uncaught error

要注意的是,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

上面代码中,throw方法抛出错误的同时执行了下一条yield语句console.log(‘b’),因为是在try...catch语句中抛出错误的,所以不影响下面语句的执行。

return方法

该方法返回给定的值并终结函数的遍历。即返回一个value为传入的参数,done为true的对象。

function* gen(){
    yield 1;
    yield 2;
    return 3;
}

var g=gen()

g.next()

// {value: 1, done: false}

g.return(4)

// {value: 4, done: true}

g.next()

// {value: undefined, done: true}

若Generator函数中有try...finally语句,则return方法使用后会等到finally块内的语句执完后才停止遍历。return方法的参数作为最后一次遍历返回的对象的value值。

function* gen(){
    yield 1;
    try{
        yield 2;
        yield 3;
    }finally{
        yield 4;
        yield 5;
    }
    yield 6;
}

var g=gen()

g.next()

// {value: 1, done: false}

g.next()

// {value: 2, done: false}

g.return(10)

// {value: 4, done: false}

g.next()

// {value: 5, done: false}

g.next()

// {value: 10, done: true}

g.next()

// {value: undefined, done: true}

上面代码中,在使用return方法后返回了value为finally语句中第一个yield表达式后的值的对象,且done为false,说明遍历还未结束。

return()除了可以手动调用外,还可以在每次迭代的末尾被任何消耗迭代器的ES6构件自动调用,如for...of和扩展预算符...。

 

yield*表达式


要在一个Generator函数里面调用另一个Generator函数,像普通函数一样调用是无效的。因为这样的调用并没有让Generator函数开始执行。

function* foo() {
  yield 'a';
  yield 'b';
}

function* bar() {
  yield 'x';
  foo();
  yield 'y';
}

for (let v of bar()){
  console.log(v);
}

// "x"

// "y"

通过使用yield*表达式,就能在一个Generator函数里面调用另一个Generator函数。

function* bar() {
  yield 'x';
  yield* foo();
  yield 'y';
}

function* foo() {
  yield 'a';
  yield 'b';
}

for (let v of bar()){
  console.log(v);
}

// "x"

// "a"

// "b"

// "y"

上面代码的过程是这样的,我们知道for...of实际上是调用了迭代器的next方法来实现遍历的,这里首先调用了bar()创建一个迭代器,然后yield*把迭代器实例控制委托给了foo(),所以在输出"x"后,下一个是"a",因为此时控制的是foo而非bar,而一旦迭代器消耗了整个foo迭代器,即将整个Generator函数执行完,就会将委托还给bar。

实际上,yield*表达式后面不只是可以放另一个Generator函数,只要是有Iterator接口的结构,都可以放在其后。yield*其实是表示遍历后面的带Iterator的结构。

function* gen(){

    yield* [1,2,3];

}

//等同于

function* gen(){

    for (let i of [1,2,3])

        yield i;

}

var g=gen();

g.next()

//{value: 1, done: false}

g.next()

//{value: 2, done: false}

g.next()

//{value: 3, done: false}

g.next()

//{value: undefined, done: true}

由此可以看出,yield*表达式其实就是用一个for...of来遍历后面的结构,同上,字符串也同样可以。

function* gen(){

    yield* 'yield';

}

var g=gen()

g.next()

// {value: "y", done: false}

g.next()

// {value: "i", done: false}

g.next()

// {value: "e", done: false}

g.next()

// {value: "l", done: false}

g.next()

// {value: "d", done: false}

g.next()

// {value: undefined, done: true}

通过使用yield*表达式调用本身,我们可以构造递归委托,如下代码

function* gen(val){
    if(val>1){
    	console.log(`val : ${val}`);
        val=yield *gen(val-1);
    }
	return val;
}
var out=gen(3);
out.next(); // val : 3
out.next(); // val : 2

作为对象的Generator函数写法


let obj = {

  myGeneratorMethod: function* () {

// ···  

}};

Generator函数的一般写法如上代码,但也可以简写为下面的代码

let obj = {

  * myGeneratorMethod() {

    //···

  }};

Generator函数的this


Generator函数返回的遍历器是Generator函数的实例,会继承Generator函数的原型,但要注意的是,函数返回的是遍历器对象,不是this对象。

function* g() {
  this.a = 11;
}

let obj = g();

obj.next();

obj.a // undefined

上面代码中,函数g中在this对象上声明赋值了一个属性a,但使用obj那不到这个a。

同时,Generator函数不能使用new运算符一起使用,否则会报错。

function* gen(){
    yield 1;
    yield 2;
}

new gen()

// Uncaught TypeError: gen is not a constructor

上面代码可以看到,报错提示gen不是一个构造函数。


参考自阮一峰的《ECMAScript6入门》

           Kyle Simpson的《你不知道的JavaScript 下卷》


ES6学习笔记目录(持续更新中)

 

ES6学习笔记(一)let和const

ES6学习笔记(二)参数的默认值和rest参数

ES6学习笔记(三)箭头函数简化数组函数的使用

ES6学习笔记(四)解构赋值

ES6学习笔记(五)Set结构和Map结构

ES6学习笔记(六)Iterator接口

ES6学习笔记(七)数组的扩展

ES6学习笔记(八)字符串的扩展

ES6学习笔记(九)数值的扩展

ES6学习笔记(十)对象的扩展

ES6学习笔记(十一)Symbol

ES6学习笔记(十二)Proxy代理

ES6学习笔记(十三)Reflect对象

ES6学习笔记(十四)Promise对象

ES6学习笔记(十五)Generator函数

ES6学习笔记(十六)asnyc函数

ES6学习笔记(十七)类class

ES6学习笔记(十八)尾调用优化

ES6学习笔记(十九)正则的扩展

ES6学习笔记(二十)ES Module的语法

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值