迭代器模式和生成器

903 阅读10分钟

前言

重看看红宝书,略微了解到迭代器和生成器这两个概念为什么那么难理解了。

首先是平时直接使用到的机会确实是非常的少,其次就是书上面为了介绍这两个概念使用了非常多的专业的名词来描述,而且相互穿插,能够把人给绕晕了,而且并没有明确的给出这些概念之间的关联。

所以我这篇文章为了辅助我的学习和记忆,着重于迭代器和生成器这关系。

首先简单的描述一下书中是如何描述这两个规范的,

迭代器导读

在迭代器中,先介绍了迭代,在 ES6 之前for和forEach两个循环的问题,引出了迭代器模式。接下来就自然的介绍迭代器模式,它包含两个概念:

  1. 第一个是可迭代协议

    介绍了它具备的能力,以及实现它的必要条件,最后介绍了当前内置的类型都有那些实现了这个可迭代协议,并且可以接受这些内置类型的原生语言特使包括那些。

  2. 第二个是迭代器协议

    介绍了它的基本概念和基本使用方法。了解了了迭代器的协议之后,也可以称呼它们为迭代器的接口,就介绍了自定义迭代器如何实现和一些细则。

但是关于两个协议的关系和职责范围没有总结出来,导致了我对于他们的关系一直很模糊,经常混淆在一起。

总结如下:

协议类型​​可迭代协议(Iterable Protocol)​​迭代器协议(Iterator Protocol)​
​目的​定义对象如何被迭代(如 for...of 遍历)定义如何按顺序访问元素(如 next() 方法)
​实现方法​对象需实现 [Symbol.iterator]() 方法对象需实现 next() 方法
​返回值​返回一个​​迭代器对象​​(符合迭代器协议)返回 { value: T, done: boolean } 对象
​核心作用​​声明对象可被迭代​​实际执行迭代过程​

也就是所以说,可迭代对象是实现了可迭代协议的对象,迭代器就是实现了迭代器协议的一系列方法。两者也是不同的,相互独立的。

而书中并没有明确的说明这一点,导致我长期认为可迭代对象是包含在迭代器中的,这是错误的认知。而且这一章节的标题只有迭代器而没有可迭代对象,也是直接阻碍我对于他们的理解。差评!

生成器导读

在生成器中,先介绍了生成器的写法,包含的元素和生命周期,存在暂停状态,存在next()方法,该方法返回的对象也是包含done属性和value属性。接下来介绍生成器最重要的组成部分:yield,被称为生成器最有用的地方。包括如下内容:

  1. 中断执行,异步变为同步的关键
  2. yiled可以传递参数
  3. 产品可迭代对象

yiled可以中断代码的进程,是它最不一样的地方,也是我们重要需要学习的地方

生成器和迭代器的关系如下

生成器是一种特殊的函数结构,它返回的对象是一个生成器对象,这个生成器对象实现了包含了可迭代对象和迭代器,换句话说这个对象它实现可迭代协议和迭代器协议, 两个协议合起来 JS 引擎对于迭代器模式的具体实现。生成器函数本质上是 JavaScript 语法规范提供的一种用户自定义的迭代器工厂函数

生成器(Generator)不是基于 ES5 的语法糖,而是 ES6(ECMAScript 2015)引入的基础语言规范,需要 JavaScript 引擎在底层实现支持。

第五版的新增了【异步迭代器和同步迭代器】这两小节,就是在说for-await-of在内部实现原理。等于把《你不知道的 JavaScript》中册对于迭代器的描述加入了进来。

class Emiter {
  constructor(max) {
    this.max = max
    this.syncIdx = 0
    this.asyncIdx = 0
  }
  // 同步迭代器
  * [Symbol.iterator]() {
    while(this.syncIdx < this.max) {
      yield this.syncIdx++
    }
  }
  // 异步迭代器
  async *[Symbol.asyncIterator]() {
    while(this.asyncIdx < this.max) {
      setTimeout(() => {
        resolve(this.asyncIdx))
      }, Math.floor(Math.random() * 1000) // 默认后台接口返回的的实现不确定的情况
    }
  }
}

使用for-await-of隐式消费 具体代码如下如下:

const emitter = new Emiter(5)
function syncCount() {
  const syncCounter = emitter[Sysmbol.iterator]()
  
  for (const x of syncCounter) {
    console.log(x)
  }
}
function asyncCount() {
  const asyncCounter = emitter[Sysmbol.asyncIterator]()
  
  for (const x of syncCounter) {
    console.log(x)
  }
}
syncCount() 
// 0
// 1
// 2
// 3
// 4
asyncCount()
// 0
// 1
// 2
// 3
// 4

一般来说,只要存在同步迭代器,也会存在异步迭代器。但是并不是说两者同时存在。

如果是我来出这一章节的话,章节名就直接以「迭代器模式和生成器」。生成器和 迭代器 从实现角度而言并不是同层级的东西,他们紧密关联,而且都是 ES新出的规范,红宝书作者可能基于这个逻辑在标题让他们并列出来。他们之间的关系我用一张图来表示,画图有助于理解:

图片.png

循环语句和迭代语句

1. 我常常把for、forEach、map、reducefor of混为一谈

2. 我常常把数组、类数组认为是可迭代对象

红宝书的 3.6 一节已经明确的说出了什么是循环语句,什么迭代语句。

for of 是干什么用的

所有人都知道一些概念for、forEach、map、reduce这些是可以遍历数组的,for of是用于遍历迭代对象的。如下:

const arr = [1, 2, 3]
arr.forEach((item, index) => {
   console.log(item) // 1, 2, 3 
   console.log(index) // 0, 1, 2
})

而巧合的是for of也可以遍历数组

for (let key of arr) {
    console.log(key) // 1 2 3
}

将arr改变为const obj = { a: 1, b: 2, c: 3 }的时候,两者都没有遍历出结果。

前者是没有反应,后者会直接报错:TypeError: obj is not iterable。翻译一下,类型错误:obj 不是一个可迭代对象。

那么什么是可迭代对象呢?

可迭代对象是什么?

我们先来看看下面这个例子:

const itemLi1 = document.getElementByTagName('li')
const itemLi2 = document.querySelectorAll('li')

for(let key of itemLi1) {
    console.log(item)
}
for(let key of itemLi2) {
    console.log(item)
}

也就是说HTMLCollectionNodeList是可以迭代对象。其他的可迭代对象有Array、map、set、string等等。如果说类数组的话,是不是迭代对象呢?

const arrLike = {
  0: 1,
  1: 2,
  2: 3,
  lenght: 3
}
for (let i = 0; i < arrLike.length; i++) {
    console.log(arrLike[i]) // 1, 2, 3
}
for (let of arrLike) {
    console.log(key) // uncachh TypeError: obj is not iterable
}

for循环打印出了对应的结果,而for of 报错了。类数组不是可迭代的的对象。这又是为什么呢?我们将类数组和HTMLCollection类型打印出来比较一下。

而类数组如下:

它们有一个明显的不同,可迭代对象的原型链上面是包括Symbol.iterator的。而这个就是让数组变成可迭代的根本原因。

也就是说,当目的对象的原型链上面包括Symbol.iterator的时候,它才是可迭代对象。

对象是无序的,无序的东西自然不可以迭代

这里使用到了Symbol类型,它在MDN上面的解释就是用于生成全局唯一的变量。而可迭代对象就是它的使用场景。受它的启发,我们在业务当中,如果需要前端来生成一个唯一ID的时候,再次之前,通常都是创建一个UUID的函数来生成一个唯一ID。Symbol不用这么麻烦,直接使用就可以了。

由此可知,Array.prototype[Symbol.iterater]这个函数封装了一些东西,使得for of可以将对象的元素给打印出来。

换一句话来说,就是Array.prototype[Symbol.iterater] = function() {}的执行生成一个迭代器对象。

也就是说,当Object.prototype也有[Symbol.iterater]的方法的时候,for of也能够遍历它呢?我们来试试看吧。

Object.ptotoype[Symbol.iterator] = function value() {}

这不就是生成器的作用么?

生成器和迭代器的关系。

生成器是一种特殊的函数结构,它返回的对象是一个迭代器。生成器函数本质上是 JavaScript 语法规范提供的一种用户自定义的迭代器工厂函数

生成器(Generator)不是基于 ES5 的语法糖,而是 ES6(ECMAScript 2015)引入的基础语言规范,需要 JavaScript 引擎在底层实现支持。

表现形式如下:

function * generation(iterableObject) {
    for(let i = 0; i < iterableObject; i++) {
        yield iterableObject[i]
    }
}

*符号和yield关键字组成。

const iterator = generation([1, 2, 3]), 其执行流程如下:

  1. iterator.next() ==> { value: 1, done: false }
  2. iterator.next() ==> { value: 2, done: false }
  3. iterator.next() ==> { value: 3, done: false }
  4. iterator.next() ==> { value: undefined, done: true }

到了第四次,value为undefined的时候,done为true(也就是说,当done为true的时候,value一定为undefined)。所以说,yield的作用有两个:

  1. 生成一个值,将该值封装成一个对象,而这个对象是{ value: .., done: flase/true }这样的形式。
  2. 停下来

可以明显的看出来,生成器有一个作用,通过next这个接口,可以看到迭代的过程。

既然说生成器生成了一个迭代器,那么是不是说生成器执行后的结果就是一个迭代器呢?既然是迭代器,自然就可以被for of给遍历。

for (const key of generation([1, 2, 3]) {
    console.log(key) // 1, 2, 3
}

果然可以。

经典面试题: 自己实现一个next这样的接口呢?

上面已经有了实现的思路。通过一个标识符和一个判断就能够使用ES5来使用,如下代码片段。

function generation(iterableObj) {
    let nextIndex = 0
    function next() {}
    
    return {
        next: () => {
            return nextIndex < iterableObj.length
                             ? { value: iterableObj[nextIndex++], done: false }
                             : { value: undefined, done: true } 
        }
    }
}

当nextIndex下于数组长度的时候,没有迭代完毕。

注意:nextIndex++是先跑nextIndex,再自增。

何为接口,后台给你一个url地址,这个是网络接口。next是设计师给你封装的一个方法,你通过这个方法来达到上吧yield的两个作用,所以next()也是一个接口,前端接口。简单来说,一个封装好的方法就是一个接口。

让非迭代对象也可以使用for of 进行遍历

正如第一节所说,Symbol.iterator的方法是迭代器的关键。那么我们也可以给Object挂载上该方法。既然该方法可以让对象变成迭代器,就可以直接使用上面ES5实现next方法的代码片段。

const obj = {
  a: 1,
  b: 2,
  c: 3
}
Object.prototype[Symbol.iterator] = function value() {
  const keys = Object.keys(Object(this))
  let nextIndex = 0

  function next() {
    return nextIndex < keys.length
        ? { value: [keys[nextIndex], obj[keys[nextIndex ++]]], done: false }
        : { value: undefined, done: true }
  }

  return {
    next
  }
}
for (const [key, value] in obj) {
  console.log(key)
}

for循环和for in的关系

for循环和for in 看着很像,其实只是共用了for这个关键字,它们都是JS引擎底层实现的东西。和forEach、map这些是基于for循环的API不同,它们是在实现在for循环之上的。

总结

  • 生成器generator执行的结果就是一个迭代器
  • 生成器可以是也是由ES5实现的,不是基于底层API
  • 是否是迭代器的关键是Symbol.iterator方法

“开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 18 天,点击查看活动详情