Generator

in js with 0 comment views: 393 times

什么是 Generator

一种可以用来控制迭代器(iterator)的函数,它可以随时暂停,并可以在任意时候恢复

function * generator(num){
  for(let i = 0; i < num; i++){
    yield console.log(i)
  }
}

var g = generator(5)

g.next() // 0
g.next() // 1
g.next() // 2
g.next() // 3
g.next() // 4

生成器函数只有在需要的时候它才会产生下一个值,而不会一次性产生所有的值。在某些情景下,这种特性十分方便。

生成器(Generator)语法

下面列出了各种可行的定义方法,不过万变不离其宗的是在函数关键词后加上一个星号。

function * generator () {}
function* generator () {}
function *generator () {}

let generator = function * () {}
let generator = function* () {}
let generator = function *() {}

// 我们不能使用箭头函数来创建一个生成器。
let generator = *() => {} // SyntaxError
let generator = ()* => {} // SyntaxError
let generator = (*) => {} // SyntaxError

下面将生成器作为方法(method)来创建。定义方法与定义函数的方式是一样的。

class MyClass {
  *generator() {}
  * generator() {}
}

const obj = {
  *generator() {}
  * generator() {}
}

yield

yield 类似 return,但又不完全相同。return 会在完成函数调用后简单地将值返回,在 return 语句之后的语句是不会执行的,你无法进行任何操作。

function withReturn(a) {
  let b = 5;
  return a + b;
  b = 6; // 不可能重新定义 b 了
  return a * b; // 这儿新的值没可能返回了
}

withReturn(6); // 11
withReturn(6); // 11

而 yield 的工作方式却不同。

function * yieldTest(a){
  let b = 5;
  yield a + b;
  b = 6;
  yield a * b;
}

var calcSix = yieldTest(6)

console.log(calcSix.next().value) // 11
console.log(calcSix.next().value) // 36

用 yield 返回的值只会返回一次,当你再次调用同一个函数的时候,它会执行至下一个 yield 语句处(译者注:前面的 yield 不再返回东西了)。

在生成器中,我们通常会在输出时得到一个对象。这个对象有两个属性:value 与 done。如你所想,value 为返回值,done 则会显示生成器是否完成了它的工作。

function * generator() {
  yield 5;
}

const gen = generator();

gen.next(); // {value: 5, done: false}
gen.next(); // {value: undefined, done: true}
gen.next(); // {value: undefined, done: true}  -之后的任何调用都会返回相同的结果

在生成器中,不仅可以使用 yield,也可以使用 return 来返回同样的对象。但是,在函数执行到第一个 return 语句的时候,生成器将结束它的工作。

function * generator() {
  yield 1;
  return 2;
  yield 3; // 到不了这个 yield 了
}

const gen = generator();

gen.next(); // {value: 1, done: false}
gen.next(); // {value: 2, done: true}
gen.next(); // {value: undefined, done: true}

yield 委托迭代

带星号的 yield 可以将它的工作委托给另一个生成器。通过这种方式,你就能将多个生成器连接在一起。

function * anotherGenerator(i) {
  yield i + 1;
  yield i + 2;
  yield i + 3;
}

function * generator(i) {
  yield* anotherGenerator(i);
}

var gen = generator(1);

console.log(gen.next().value); // 2
console.log(gen.next().value); // 3
console.log(gen.next().value); // 4

yield 可以在 next() 方法调用后返回传递的值:

function * generator(arr) {
  for (const i in arr) {
    console.log('0--',i)
    yield i;
    console.log('1--',i)
    yield yield yield;
    console.log('2--',i)
    yield(yield);
    console.log('3--',i)
  }
}

var gen = generator([0,1]);

console.log(gen.next())
console.log(gen.next('a'))
console.log(gen.next('b'))
console.log(gen.next('c'))
console.log(gen.next('d'))
console.log(gen.next('e'))

console.log(gen.next())
console.log(gen.next('f'))
console.log(gen.next('g'))
console.log(gen.next('h'))
console.log(gen.next('i'))
console.log(gen.next('j'))

结果如下图

image

你可以看到 yield 默认是 undefined,但如果我们在调用 yield 时传递了任何值,它就会返回我们传入的值。我们将很快利用这个特性。

初始化与方法

生成器是可以被复用的,但是你需要对它们进行初始化。

function * generator(arg = 'Nothing') {
  yield arg;
}

var gen0 = generator(); // OK
console.log(gen0.next()) // Object { value: "Nothing", done: false }

var gen1 = generator('Hello'); // OK
console.log(gen1.next()) // Object { value: "Hello", done: false }

var gen2 = new generator(); // 报错 Exception: TypeError: generator is not a constructor

console.log(generator().next()); // 可以运行,但每次都会从头开始运行 Object { value: "Hello", done: false }

如上所示,gen0 与 gen1 不会互相影响,gen2 会报错。因此初始化对于保证程序流程的状态是十分重要的。

next() 方法

function * gen(){
  yield 1;
  yield 2;
  yield 3;
}
var gen = gen()
console.log(gen.next()) // Object { value: 1, done: false }
console.log(gen.next()) // Object { value: 2, done: false }
console.log(gen.next()) // Object { value: 3, done: false }
console.log(gen.next()) // Object { value: undefined, done: true } 之后所有的 next 调用都会返回同样的输出

这是最常用的方法。它每次被调用时都会返回下一个对象。在生成器工作结束时,next() 会将 done 属性设为 true,value 属性设为 undefined。

我们不仅可以用 next() 来迭代生成器,还可以用 for of 循环来一次得到生成器所有的值(而不是对象)。

function * gen(arr){
  for(let i in arr){
    yield i;
  }
}

var g = gen([0,1,2])

for(let i of g){
  console.log(i) // 0 1 2
}

console.log('g.next-->>', g.next()) // g.next-->> Object { value: undefined, done: true }

但它不适用于 for in 循环,并且不能直接用数字下标来访问属性:generator[0] = undefined。

return() 方法

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

var g = gen()

console.log(g.return()) //Object { value: undefined, done: true }
console.log(g.return('genReturn')) // Object { value: "genReturn", done: true }
console.log(g.next()) // Object { value: undefined, done: true }  - 在 return() 之后的所有 next() 调用都会返回相同的输出

return() 将会忽略生成器中的任何代码。它会根据传值设定 value,并将 done 设为 true。任何在 return() 之后进行的 next() 调用都会返回 done 属性为 true 的对象。

throw() 方法

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

var g = gen()

console.log(g.throw('something err')) // Exception: something err
console.log(g.next()) // Object { value: undefined, done: true }

throw() 做的事非常简单 —— 就是抛出错误。我们可以用 try-catch 来处理。

自定义方法的实现

由于我们无法直接访问 Generator 的 constructor,因此如何增加新的方法需要另外说明。

function * generator() {
  yield 1;
}

generator.prototype.__proto__; // Generator {constructor: GeneratorFunction, next: ƒ, return: ƒ, throw: ƒ, Symbol(Symbol.toStringTag): "Generator"}

// 由于 Generator 不是一个全局变量,因此我们只能这么写:
generator.prototype.__proto__.math = function(e = 0) {
  return e * Math.PI;
}

generator.prototype.__proto__; // Generator {math: ƒ, constructor: GeneratorFunction, next: ƒ, return: ƒ, throw: ƒ, …}

const gen = generator();
gen.math(1); // 3.141592653589793

生成器的用途

返回随机数的函数

function * randomFrom(...arr) {
  while (true)
    yield arr[Math.floor(Math.random() * arr.length)];
}

var random = randomFrom(1, 2, 5, 9, 4);

console.log(random.next().value); // 返回随机的一个数

节流(throttle)函数

function * throttle(func, time) {
  let timerID = null;
  function throttled(arg) {
    clearTimeout(timerID);
    timerID = setTimeout(func.bind(window, arg), time);
  }
  while (true)
    throttled(yield);
}

const thr = throttle(console.log, 1000);

thr.next(); // {value: undefined, done: false}
thr.next('hello'); // 返回 {value: undefined, done: false} ,然后 1 秒后输出 'hello'

斐波那契数列

function * fibonacci(seed1, seed2) {
  while (true) {
    yield (() => {
      seed2 = seed2 + seed1;
      seed1 = seed2 - seed1;
      return seed2;
    })();
  }
}

const fib = fibonacci(0, 1);
fib.next(); // {value: 1, done: false}
fib.next(); // {value: 2, done: false}
fib.next(); // {value: 3, done: false}
fib.next(); // {value: 5, done: false}
fib.next(); // {value: 8, done: false}

将生成器用在 HTML 上

const strings = document.querySelectorAll('.string');
const btn = document.querySelector('#btn');
const className = 'darker';

function * addClassToEach(elements, className) {
  for (const el of Array.from(elements))
    yield el.classList.add(className);
}

const addClassToStrings = addClassToEach(strings, className);

btn.addEventListener('click', (el) => {
  if (addClassToStrings.next().done)
    el.target.classList.add(className);
});

参考:https://juejin.im/post/5b14faf2f265da6e4d5af3b9


Responses