欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

ECMAScript 6(21)Generator 函数的语法

程序员文章站 2022-06-13 08:00:30
...

1. 是什么

  • 调用 Generator 函数后,该函数并不执行
  • 调用后不反回结果, 返回一个对象, 遍历器对象(Iterator Object)。
  • 对象调用next()方法,进入下一步. 简单说, Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。
  • 如果没有return语句,就执行到函数结束
  • done属性是一个布尔值,表示是否遍历结束。done属性的值true,表示遍历已经结束。
  • yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。
function* test(){
    yield 'hello';
    yield 'world';
    return 'end'; 
}
var tt = test()
tt.next() // {value: "hello", done: false}
tt.next() // {value: "world", done: false}
tt.next() // {value: "end", done: false}
tt.next() // {value: undefined, done: true}
  • Generator 函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数。
function* f() {
  console.log('执行了!')
}

var generator = f();

setTimeout(function () {
  generator.next()
}, 2000);
  • yield表达式如果用在另一个表达式之中,必须放在圆括号里面。
function* demo() {
  console.log('Hello' + yield); // SyntaxError
  console.log('Hello' + yield 123); // SyntaxError

  console.log('Hello' + (yield)); // OK
  console.log('Hello' + (yield 123)); // OK
}
  • yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号。
 function* demo() {
  foo(yield 'a', yield 'b'); // OK
  let input = yield; // OK
}
  • 任意一个对象的Symbol.iterator方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。
var myIterable = {};
myIterable[Symbol.iterator] = function* () {
  yield 1;
  yield 2;
  yield 3;
};

[...myIterable] // [1, 2, 3]
  • Generator 函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator属性,执行后返回自身。
function* gen(){
    yield 1;
    yield 2;
}

var g = gen();

[...g]  // [1,2]

2. next()

  • next方法的参数表示上一个yield表达式的返回值
  • 所以在第一次使用next方法时,传递参数是无效的。V8 引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数。
function* foo(x) {
  var y = 2 * (yield (x + 1));
  var z = yield (y / 3);
  return (x + y + z);
}

var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}

var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }

3. for…of

  • for…of循环可以自动遍历 Generator 函数运行时生成的Iterator对象,且此时不再需要调用next方法。
  • 一旦next方法的返回对象的done属性为true,for…of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回的6,不包括在for…of循环之中。
function* foo() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
  return 6;
}

for (let v of foo()) {
  console.log(v);
}
// 1 2 3 4 5
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

// for...of 循环
for (let n of numbers()) {
  console.log(n)
}
// 1
// 2

4. Generator.prototype.throw()

  • Generator 函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。
  • throw方法可以接受一个参数,该参数会被catch语句接收,建议抛出Error对象的实例。
  • throw方法抛出的错误要被内部捕获,前提是必须至少执行过一次next方法。
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
  • 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
  • 一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。简单说就是里面报错没有在里面catch到,函数就直接结束.
  1. 遍历器的throw方法可以将错误传入Generator函数内部;
  2. Generator函数内部可以通过try…catch来捕获错误,如果没有被捕获,将冒泡到Generator函数外面来;
  3. 如果在Generator函数内部正常捕获了错误,那么代码将继续执行下去;
  4. 如果没有捕获到错误,遍历器的done将变为true;

5.Generator.prototype.return()

  • Generator 函数返回的遍历器对象,还有一个return方法,可以返回给定的值,并且终结遍历 Generator 函数。
  • return方法调用时,不提供参数,则返回值的value属性为undefined
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 }

6. yield*

如果在 Generator 函数内部,调用另一个 Generator 函数。需要在前者的函数体内部,自己手动完成遍历。

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

function* bar() {
  yield 'x';
  // 手动遍历 foo()
  for (let i of foo()) {
    console.log(i);
  }
  yield 'y';
}

for (let v of bar()){
  console.log(v);
}
// x
// a
// b
// y
  • ES6 提供了yield*表达式,作为解决办法,用来在一个 Generator 函数里面执行另一个 Generator 函数。
  • yield*后面的 Generator 函数(没有return语句时),等同于在 Generator 函数内部,部署一个for…of循环。
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';
}

for (let v of bar()){
  console.log(v);
}
// "x"
// "a"
// "b"
// "y"
  • 有return语句时,则需要用var value = yield* iterator的形式获取return语句的值。
function* foo() {
  yield 2;
  yield 3;
  return "foo";
}

function* bar() {
  yield 1;
  var v = yield* foo();
  console.log("v: " + v);
  yield 4;
}

var it = bar();

it.next()
// {value: 1, done: false}
it.next()
// {value: 2, done: false}
it.next()
// {value: 3, done: false}
it.next();
// "v: foo"
// {value: 4, done: false}
it.next()
// {value: undefined, done: true}

7. 作为对象属性的 Generator 函数

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

8. this

  • Generator函数的this指向的是window
function *g() {
    console.log(this)
    yield 'g'
}

let foo = g()
foo.next()
// Window
// {value: "g", done: false}
  • Generator函数不能通过new来生成遍历器(会报错),所以改变this指向之可以用 call,apply,bind
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
  • 另一种是直接绑在 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

9. 状态机

  • 这里的状态机通常指有限状态机
  • javascript 通常的操作就是定义一个全局变量flag , 然后通过flag = !flag 改变状态,有点类似与jquery 里面的toggle操作
var flag = true;
var toggle = function(){
    if(flag)
        // show
    else
        // hide

    flag = !flag
}
  • 使用 Generator 实现
var toggle = function* () {
  while (true) {
    // show
    yield;
    // hide
    yield;
  }
};
  • 优点不用定于全局状态变量, 这样就更简洁,更安全(状态不会被非法篡改)、更符合函数式编程的思想,在写法上也更优雅。

10. 应用

  • Generator函数的主要目的就是用于解决异步编程,可以极大的减轻异步编程的撰写难度。
  • 举例 假如我们有5个异步请求,并且后一个需要根据前一个异步请求的结果来完成请求,那么代码的复杂度将急剧上升。
function delay(msg, callback) {
    setTimeout(function () {
        console.log(msg)
        callback()
    }, 1000)
}
delay('1', function () {
    delay('2', function () {
        delay('3', function () {
            delay('4', function () {
                delay('5', function () {
                    console.log('done')
                })
            })
        })
    })
})
  • es6新增的promise,在解决异步编程时,降低了一定难度,但主要是降低了对于单个异步请求的难度,看起来简单轻松。也增强了对错误处理的能力,但对于以上情况的解决,并没有质的提升。
function delay(msg) {
    return new Promise((resolve, reject) => {
        setTimeout(function () {
            console.log(msg)
            resolve()
        }, 1000)
    })
}
delay('1')
    .then(() => delay('2'))
    .then(() => delay('3'))
    .then(() => delay('4'))
    .then(() => delay('5'))
    .then(() => {
        console.log('done')
    })

– 上面代码耦合度高,而且要求每个异步都是Promise,不然不可行;
– 于是,Generator函数出现了,是解决此类场景的极佳办法;

function delay(msg) {
    setTimeout(function () {
        iter.next(msg)
    }, 1000)
}

function*g() {
    let result1 = yield delay('1')
    console.log(result1)
    let result2 = yield delay('2')
    console.log(result2)
    let result3 = yield delay('3')
    console.log(result3)
    let result4 = yield delay('4')
    console.log(result4)
    let result5 = yield delay('5')
    console.log(result5)
    console.log('done')
}
let iter = g()
iter.next()
  • 这个方法具备以下特点:
  1. 像同步代码一样写多个异步请求,同步编程写法显然比Promise或者回调函数套用要简单;
  2. 解耦,每个异步函数在最后加上一个iter.next()方法,用于将状态机推到下一个状态,但却不必关心下一个状态是哪个异步请求;
  3. 通过iter.next()的参数,可以将上一次请求的数据传给下一次请求的数据,简单暴力;
  4. 通过函数的参数(比如dalay(result1))来将上一次异步请求的结果传给当前这次异步请求使用。

10.2 部署遍历器接口

let obj = {
    a: 1,
    b: 2,
    c: 3,
    *[Symbol.iterator]() {
        yield this.c
        yield this.b
        yield this.a
    }
}
let arr = [...obj];
arr;    // [3, 2, 1]

参考文献:阮一峰es6

相关标签: ES6系列 es6