带你重学ES6 | Generator

1,001 阅读5分钟

Generator 是 ES6 提出的一种异步编程的解决办法,它与传统的函数完全不同,本章从基础概念和基本用法进行讲解和解析。在此之前也是对 Generator 函数云里雾里,所以通过此次学习,希望能对 Generator 有更深的理解和认识。

1.概念

Generator 中文的意思是'生成器',阮一峰:ECMAScript 6 入门中对 Generator 解释是: Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。

执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。 Generator 有两个特点: 第一个是在定义时,要在 function 关键字和函数名中加一个星号,第二个就是在函数体中运用 yeild 表达式表示不同的状态。

function* foo() {
  yield "a";
  yield "b";
}
let f = foo();
console.log(f);

和普通函数不同的是,当运行这个函数时,返回不是这个函数的结果,而是一个遍历器对象,或者可以说是一个含有内部状态指针的对象。 如果想输出值得话,要用 next 方法来进行输出。next 方法就是向下移动指针,即每次调用 next 方法,就是从函数头部或者从上一次 yield 表达式移动到下一次 yield 表达式(或者 return 为止)。

function* foo() {
  yield "a";
  yield "b";
}
let f = foo();
f.next(); // {value: "a", done: false}
f.next(); // {value: "b", done: false}
f.next(); // {value: undefined, done: true}

上述可以看出,调用 next 时输出的是一个对象,即 value 值代表 yield 后面的结果,done 代表遍历还没有结束,当遍历结束时,value 值都为 undefined,done 都为 true。

2.yield 表达式

yield 有个懒惰的特性,即 yield 后面的表达式,如果不调用 next 方法,是不会执行的。

function* foo() {
  yield 1 + 1;
}
foo().next(); // 2

只有当 next 指针移动到该 yield 的时候,才会执行后面的表达式。 yield 和 return 是有不同之处的,在 Generator 函数中,可以定义多个 yield 表达式,但是 return 只能定义一个,并且在 yield 中遍历还没有完成,但在遇到 return 时,遍历就终止了。

function* foo() {
  yield "a";
  yield "b";
  return "c";
}
let f = foo();
f.next(); // {value: "a", done: false}
f.next(); // {value: "b", done: false}
f.next(); // {value: "c", done: true}
f.next(); // {value: undefined, done: true}

当遇到 return 时,遍历结束 done 为 true,value 值为 return 后的结果,在此之后的 next 的结果都为{value: undefined, done: true}。 如果在 Generator 函数中没有 yield 表达式:

function* foo() {
  console.log("a");
}
let f = foo();
f.next();

foo()返回的依旧是一个含有内部状态指针的对象,只有当 next 方法执行时该函数才会执行。 当 yield 和其他表达式融合时,如果 yield 表达式在左边,要将 yield 表达式用圆括号包裹,否则就会报语法错误。

function* foo() {
  console.log('a'+ yield 'b'); // Uncaught SyntaxError: Unexpected identifier
}
function* foo() {
  console.log('a'+ (yield 'b')); // {value: "b", done: false}
}
let f = foo();
f.next();

当用作函数参数或放在赋值表达式的右边,可以不加括号。

function* demo() {
  foo(yield "a", yield "b"); // OK
  let input = yield; // OK
}

3.next 方法

yield 是没有返回值的,它的返回值是 undefined,我们可以通过 next 方法将参数传递给 yield,此参数将会为 yield 的返回值。

function* f() {
  for (var i = 0; true; i++) {
    var reset = yield i;
    if (reset) {
      i = -1;
    }
  }
}
var g = f();
g.next(); // { value: 0, done: false }
g.next(); // { value: 1, done: false }
g.next(true); // { value: 0, done: false }

上面代码先定义了一个可以无限运行的 Generator 函数 f,如果 next 方法没有参数,每次运行到 yield 表达式,变量 reset 的值总是 undefined。当 next 方法带一个参数 true 时,变量 reset 就被重置为这个参数(即 true),因此 i 会等于-1,下一轮循环就会从-1 开始递增。 不用 next 方法是否可以输出值呢?答案是可以的,可以用 for...of...方法遍历 Generator 函数。

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
}

值得注意的是,for...of...在 done 为 ture 的时候就会终止执行,所以 return 后的 6 没有输出。

4.yield* 表达式

yield* 表达式是为了解决在一个 Generator 函数中调用另一个 Generator 函数所提供的方法。

function* foo() {
  yield 1;
  yield 2;
  yield 3;
  yield* foo1();
}
等同于;
function* foo() {
  yield 1;
  yield 2;
  yield 3;
  for (let v of foo1()) {
    yield v;
  }
}

当 yield*后边的 Generator 函数中没有 return 时,作用就是 for...of...遍历 Generator 函数,如果 Generator 函数中有 return 时,则获取的是 return 值。

function* foo() {
  yield 1;
  yield 2;
  yield 3;
  var value = yield* foo1();
  console.log(value);
}
function* foo1() {
  yield 4;
  return 5;
}
let f = foo();
f.next(); // {value: 1, done: false}
f.next(); // {value: 2, done: false}
f.next(); // {value: 3, done: false}
f.next(); // 5 {value: undefined, done: true}

其实 yield*后面只要是带有 Iterator 接口的都会被遍历。

function* foo() {
  yield* [1, 2, 3, 4];
}
let f = foo();
f.next(); // {value: 1, done: false}

再举个例子:

function* foo() {
  yield "a";
  yield "b";
  return "END";
}
function* bar(func) {
  let result = yield* func();
  console.log(result);
}
[...bar(foo)];
// END
// ["a", "b"]

上述中 foo 是拥有 return 表达式的函数,所以 return 后的结果会‘赋值’给 yield*表达式的返回值,所以 result 是'END',并且拓展运算符默认调用 Iterator 接口,所以会先打印出 result,然后再执行 yield。

参考:

阮一峰:ECMAScript 6 入门

后语

相关文章:

觉得还可以的,麻烦走的时候能给点个赞,大家一起学习和探讨!

还可以关注我的博客希望能给我的github上点个Start,小伙伴们一定会发现一个问题,我的所有用户名几乎都与番茄有关,因为我真的很喜欢吃番茄❤️!!!

想跟车不迷路的小伙还希望可以关注公众号 前端老番茄 或者扫一扫下面的二维码👇👇👇。

我是一个编程界的小学生,您的鼓励是我不断前进的动力,😄希望能一起加油前进。

本文使用 mdnice 排版