48. ES6+ Generator 基础知识

1. 前言

前面我们花了三节深入地学习了 ES6 的异步解决方案 Promise,本节学习的生成器也是为了解决异步而生的,但是它的出发思路和 Promise 截然不同。

上节我们学习了 ES6 中 迭代 的相关内容,并实现了一个迭代器。我们知道实现一个迭代器,我们需要手动添加对象的 Symbol.iterator 属性,并需要实现 next 方法。那么有没有什么可以帮助我们自动实现迭代器呢?ES6 给出了生成器的方法来满足我们的需求。我们不需要在对象上添加 Symbol.iterator 属性,使用生成器函数就可以实现迭代器的功能。本节我们将学习生成器的相关概念和基础用法。

生成器是一个灵活的结构,能使得一个函数块内部暂停和恢复代码执行的能力。在实际应用中,使用生成器可以自定义迭代器和协程。

2. 生成器对象和生成器函数

有些概念是我们必须要理解的,前面在学习迭代器的时候,我们学习了迭代协议和迭代器协议,实现一个迭代器需要满足这两个协议才算是一个真正的迭代器。而本节的生成器和生成器函数也是如此,我们也需要知道生成器对象和生成器函数概念和它们直接的关系。

Generator 就是我们说的生成器,它包含两个概念 生成器对象和生成器函数。首先,要理解的是生成器对象和迭代器的关系,生成器对象是遵守迭代协议和迭代器协议实现的 Iterable 接口,可以理解生成器对象其实也是一个迭代器;然后,我们需要理解什么是生成器函数,生成器函数是由 function * 来定义的,并且返回结果是一个 Generator 对象。

生成器是一个特殊的函数,在调用后会返回一个生成器对象,这个生成器对象是遵守可迭代协议和迭代器协议实现的 Iterable 接口。生成器可以使用 yield 关键字来暂停执行的生成器函数:

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

var gen = generator();	// Object [Generator] {}

2.1 Generator.prototype.next()

生成器的 next() 方法和迭代器返回的结果是一样的,返回了一个包含属性 done 和 value 的对象,该方法也可以通过接受一个参数用以向生成器传值。

使用 yield 返回的值会被迭代器的 next() 方法捕获:

var gen = generator();

gen.next(); // {value: 'a', done: false}
gen.next(); // {value: 'b', done: false}
gen.next(); // {value: undefined, done: true}

从上面代码的执行结果可以看出,生成器函数在执行后会返回一个生成器对象,这个生成器对象满足迭代协议和迭代器协议,所以我们可以去手动调用它的 next() 方法去获取每一步的返回值。从这里可以看出,生成器其实就是迭代器的一个应用,并且这个应用会在异步中大放异彩。

2.2 Generator.prototype.return()

return() 方法返回给定的值并结束生成器。

var gen = generator();

gen.next(); // { value: 'a', done: false }
gen.return("imooc"); // { value: "mybj", done: true }
gen.next(); // { value: undefined, done: true }

另外,如果对已经完成状态的生成器调用 return(value) 则生成器会一直保持在完成状态,如果出入参数,value 会设置成传入的参数,done 的值不变:

var gen = generator();

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

2.2 Generator.prototype.throw()

throw() 方法用来向生成器抛出异常,并恢复生成器的执行,返回带有 done 及 value 两个属性的对象。

function* generator() {
  while(true) {
    try {
       yield 'mybj'
    } catch(e) {
      console.log("Error caught!");
    }
  }
}
var gen = generator();
gen.next(); // { value: "mybj", done: false }
gen.throw(new Error("error")); // "Error caught!"

3. Generator 案例

3.1 类数组转化

将一个类数组转化为一个真正的数组方式有很多,ES6 提供了 Array.from() 可以将类数组转化为数组 。另外在一些函数中可以使用 [...argument] 的方式转化类数组。

function fn() {
  const arg = [...arguments];
  console.log(arg);
}
fn(1, 2, 3); // [1, 2, 3]

当然我们知道类数组的定义,所以我们自己定义一个类数组,看能不能使用展开运算符将类数组转化为数组:

const likeArr = {
  0: 1,
  1: 2,
  length: 2,
}
console.log([...likeArr]); // Uncaught TypeError: likeArr is not iterable

上面代码中我们定义了一个类数组,但是使用展开运算符报错了,提示我们 likeArr 不是一个迭代器。因为在函数中类数组是内部帮我们实现了迭代器的功能,而我们自己定义的类数组是不具有迭代器功能的,那我们来自己实现一个:

likeArr[Symbol.iterator] = function() {
  let index = 0;
  return {
    next: () => {
      return { value: this[index], done: index++ === this.length}
    }
  }
}
console.log([...likeArr]); // [1, 2]

上面的代码我们在 likeArr 对象上定义了 Symbol.iterator 它具有迭代功能。上面代码中我们需要手动地去实现 next() 方法,这比较麻烦,那能不能简化一下呢?我们的生成器函数就出场了:

likeArr[Symbol.iterator] = function* () {
  let index = 0;
  while (index !== this.length) {
    yield this[index++];
  }
}
console.log([...likeArr]); // [1, 2]

上面的代码使用了生成器函数,并且没有去手动实现 next() 方法,从这里我们也能很清楚地知道迭代器和生成器的关系。而且使用生成器函数更加简单方便。

3.2 单步获取质数

还有一个案例是面试中经常会考到的。

题目:实现一个函数,每次调用返回下一个质数,要求不使用全局变量,且函数本身不接受任何参数。

从题目的要求可以知道,这个函数每次调用都会返回一个质数,也就是说每次调用后都会返回一个函数。

首先我们定义一个判断一个数是否为质数的方法:

function isPrime(num) {
  for (let i = 2; i <= Math.sqrt(num); i++) {
    if (num % i === 0) {
      return false
    }
  }
  return true
}

传统的方式是使用闭包方法来解决:

function primeHandler() {
  let prime = 1
  return () => {
    while (true) {
      prime++
      if (isPrime(prime)) {
        return prime
      }
    }
  }
}

const getPrime = primeHandler()
console.log(getPrime()); // 2
console.log(getPrime()); // 3
console.log(getPrime()); // 5

既然是单步执行的,那么我们就可以使用迭代器方式实现:

var prime = {}
prime[Symbol.iterator] = function() {
  let prime = 1;
  return {
    next() {
      while(true) {
        prime++
        if (isPrime(prime)) {
          return prime;
        }
      }
    }
  }
}
var getPrime = prime[Symbol.iterator]().next;
console.log(getPrime()); // 2
console.log(getPrime()); // 3

上一个实例我们知道实现迭代器的方式是很麻烦的,可以使用生成器函数去替代迭代器的功能,所以上面的代码可以使用生成器函数改造如下:

function* primeGenerator () {
  let prime = 1
  while (true) {
    prime++
    if (isPrime(prime)) {
      yield prime
    }
  }
}

var getPrime = primeGenerator().next().value
console.log(getPrime()); // 2
console.log(getPrime()); // 3

4. 小结

本节我们主要学习了生成器的概念和用法,需要生成器对象是由生成器函数返回的结果,生成器对象是遵守迭代协议和迭代器协议实现的 Iterable 接口。生成器其实就是对迭代器的应用。另外,通过两个案例更加深刻地理解了生成器的应用场景,对比了生成器和迭代器的不同。

「点点赞赏,手留余香」

0

给作者打赏,鼓励TA抓紧创作!

微信微信 支付宝支付宝

还没有人赞赏,快来当第一个赞赏的人吧!

声明:
1. 本站所有文章教程及资源素材均来源于网络与用户分享或为本站原创,仅限用于学习和研究。
2. 如果内容损害你的权益请联系客服QQ:1642748312给予处理。
码云笔记 » 48. ES6+ Generator 基础知识

发表回复

IT互联网行业相关广告投放 更专业 更精准

立即查看 联系我们