javascript-Generator语法

loading 2022年11月16日 66次浏览

1.简介

Generator函数是ES6提供的一种异步编程解决方案。

语法上,可以把它理解成,Generator函数是一个状态机,封装了多个内部状态。

执行Generator函数会返回一个遍历器对象,也就是说,Generator函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历Generator函数内部的每一个状态。

Generator函数有两个特征:
(1)funtion关键字和函数名之间有个*号
(2)函数体内部使用yield表达式,定义不同的内部状态

下面这个生成器函数内部有两个yield表达式,因此该函数有3个状态:hello,world和return 语句。

执行Generator函数后,返回一个迭代器对象传给hw。

        const myGenerator = function* () {
            yield 'hello';
            yield 'world';
            return 'end';
        }
        const hw = myGenerator();

下一步,通过迭代器对象的next方法,移动指针至下一个状态。

        hw.next(); // { value: 'hello', done: false }
        hw.next(); // { value: 'world', done: false }
        hw.next(); // { value: 'ending', done: true }
        hw.next(); // { value: undefined, done: true }

也就是说,每次调用next方法,指针就从上一次停下来的地方执行,直到遇到下一个yield/return。

换言之,Generator函数是分段执行的,yield/return表达式是暂停执行的标记,next方法用于恢复执行

2.yield和next

2.1 yield基本作用

首先yield用来暂停,next用来继续上文已提,这里带过。

yield后面的表达式只有调用next方法使得内部指针指向该语句时才会执行。

比如这样,console语句不会先执行,而是遍历到它了才打印3。

        const myGenerator = function* () {
            yield 'hello';
            yield console.log(1+2);
            return 'end';
        }

如果Generator函数内没有yield表达式,就起到暂缓执行的作用。

例如这个例子,如果是普通函数的话,在为it赋值时就会执行;因为是Generator函数,所以只有在调用next方法时才会执行。

        const myGenerator = function* () {
            console.log('暂缓执行')
        }

        const it = myGenerator();
        
        setTimeout(()=>{
            it.next();
        } , 1000)

2.2 与Iterator接口的关系

如果直接用扩展运算符遍历一个对象会报错,因为对象不具有Symbol.iterator属性,也就不具有Iterator接口。

        const obj = { 'a': 1, 'b': 2 };
        console.log(...obj)
	//Uncaught TypeError: Spread syntax requires ...iterable[Symbol.iterator] to be a function

由于Generator就是生成遍历器的函数,因此可以把Generator赋值给对象的Symbol.iterator属性,使其具有Iterator接口。

        const obj = { 'a': 1, 'b': 2 };       
       
        obj[Symbol.iterator] = function* () {
            yield 1;
            yield 2;
            yield 3;
        };

        console.log(...obj) // 1 2 3

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

        function* gen() {
            // some code
        }

        var g = gen();

        g[Symbol.iterator]() === g //true

2.3 next方法的参数

yield的返回值始终是undefined(注意例如yield 123并不是返回123 而是通过指针才能访问到value:123)。

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

例如这个例子,如果next方法没有参数,那么每次运行到yield,reset的值始终是undefined。直到next方法中传入参数true,使得reset == true,使得i == -1,因此下一轮循环就会从-1开始递增。

        const fn = function* () {
            for (let i = 0; true; i++) {
                const reset = yield i;
                if (reset) i = -1;
            }
        }
        const it = fn();
        console.log(it.next()); //{value: 0, done: false}
        console.log(it.next()); //{value: 1, done: false}
        console.log(it.next()); //{value: 2, done: false}
        console.log(it.next(true)); //{value: 0, done: false}

通过这个技巧,就有办法在Generator函数开始运行之后,继续向函数体内部注入值,从而调整函数行为。

        const foo = function* (x) {
            let y = 2 * (yield (x + 1));
            let z = yield (y / 3);
            return x + y + z;
        }
        //由于第二个it1.next()不带参数 导致y = 2*undefined = NaN
        const it1 = foo(5);
        it1.next() //{value:6, done:false}
        it1.next() //{value:NaN, done:false}
        it1.next() //{value:NaN, done:true}
        
        const it2 = foo(5);
        it2.next(); //{value:6, done:false}
        //令yield(x+1)=12 则y=24 z = yield 8
        it2.next(12); //{ value:8, done:false }
        //令yield(y/3)=13 则z=13 x+y+z=5+24+13=42
        it2.next(13); // { value:42, done:true }

或者是这样传参

        function* dataConsumer() {
            console.log('Started');
            console.log(`1. ${yield}`);
            console.log(`2. ${yield}`);
            return 'result';
        }

        let genObj = dataConsumer();
        genObj.next();
        // Started
        genObj.next('a')
        // 1. a
        genObj.next('b')
        // 2. b

3. for...of遍历Generator

因为Generator函数生成Iterator对象,所以可以通过for...of来遍历。

        function* foo(){
            yield 1;
            yield 2;
            yield 3;
            return 4;
        }
        for(let v of foo()){
            console.log(v); //1 2 3
        }

除了for...of,扩展运算符,解构赋值,Array.from等方法内部调用的都是遍历器接口,意味着都可以将Generator返回的Iterator对象作为参数。

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

        // 扩展运算符
        [...numbers()] // [1, 2]

        // Array.from 方法
        Array.from(numbers()) // [1, 2]

        // 解构赋值
        let [x, y] = numbers();
        //x = 1
        //y = 2

4. Generator.prototype.throw()

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

相当于将yield替换成一个throw语句

        const g = function* (x, y) {
            let result = yield x + y;
            return result;
        };
        gen.throw(new Error('出错了')); // Uncaught Error: 出错了
        // 相当于将 let result = yield x + y
        // 替换成 let result = throw(new Error('出错了'));

如这个例子,it连续throw两个错误,第一个被函数体内catch捕获;第二次throw由于内部的catch语句已经执行过了,错误就被抛出函数体,被外部的catch捕获。

        const myGenerator = function*(){
            try{
                yield;
            }catch(e){
                console.log('内部捕获' , e);
            }
        }
        const it = myGenerator();
        it.next();

        try{
            it.throw('aaa');
            it.throw('bbb');
        }catch(e){
            console.log('外部捕获' , e);
        }

要注意的是throw方法抛出的错误要被内部捕获,前提是必须至少执行过一次next方法。如果把那句it.next()去掉函数内部就捕获不到了。

但是要注意如果是用全局的throw抛出,那么就只能被外部捕获。(当然catch也只能捕获一次)

        try {
            throw new Error('a');
            throw new Error('b');
        } catch (e) {
            console.log('外部捕获', e);
        }

throw方法被捕获以后,会附带执行下一条yield表达式。也就是说,会附带执行一次next方法。

        const myGenerator = function* () {
            try {
                yield console.log('a');
            } catch (e) {
                console.log('内部捕获', e);
            }
            yield console.log('b');
        }
        const it = myGenerator();
        it.next(); //a
        it.throw('sorry'); 
        //内部捕获 sorry
        //b

一旦执行过程中错误抛出并没有被内部捕获,就不会执行下去了。如果此后还调用next方法,将返回一个value属性等于undefined、done属性等于true的对象。

        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

5. 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 }

如果Generator函数内部有try...finally代码块,且正在执行try代码块,那么return()方法会导致立刻进入finally代码块,执行完以后,整个函数才会结束。

        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 }

其实就相当于将yield表达式替换成一个return语句

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

6.yield*

当在一个Generator内部要调用另一个Generator时用到的表达式。

        function* foo() {
            yield 'a';
            yield 'b';
        }
        function* bar() {
            yield 'x';
            yield* foo();
            yield 'y';
        }
        //等同于
        function* bar() {
            yield 'x';
            yield 'a';
            yield 'b';
            yield 'y';
        }
        //等同于
        function* bar() {
            yield 'x';
            for (let v of foo()) {
                yield v;
            }
            yield 'y';
        }

同理,任何数据结构只要具有Iterator接口,就可以被yield*遍历

        function* gen() {
            yield* ["a", "b", "c"];
        }
        //遍历数组
        gen().next() // { value:"a", done:false }

        let read = (function* () {
            yield 'hello';
            yield* 'hello';
        })();
        //遍历字符串
        read.next().value // "hello"
        read.next().value // "h"

7.应用

7.1 用同步方式写异步

Generator 函数的暂停执行的效果,意味着可以把异步操作写在yield表达式里面,等到调用next方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在yield表达式下面,反正要等到调用next方法时再执行。

        function* loadUI() {
            showLoadingScreen();
            yield loadUIDataAsynchronously();
            hideLoadingScreen();
        }
        var loader = loadUI();
        // 加载UI
        loader.next()

        // 卸载UI
        loader.next()

逐行读取文本文件

        function* numbers() {
            let file = new FileReader("numbers.txt");
            try {
                while (!file.eof) {
                    yield parseInt(file.readLine(), 10);
                }
            } finally {
                file.close();
            }
        

7.2 控制流管理(任务均为同步操作)

回调地狱

        step1(function (value1) {
            step2(value1, function (value2) {
                step3(value2, function (value3) {
                    step4(value3, function (value4) {
                        // Do something with value4
                    });
                });
            });
        });

Promise

        Promise.resolve(step1)
            .then(step2)
            .then(step3)
            .then(step4)
            .then(function (value4) {
                // Do something with value4
            }, function (error) {
                // Handle any error from step1 through step4
            })
            .done();

Generator

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

7.3 作为数据结构

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是一个函数,可以像回调函数那样使用它
        }