查看原文
其他

前端人 70% 以上 不了解的迭代器/可迭代对象/生成器

推荐关注↓

作者:一只小ice

链接:https://juejin.cn/post/7141685685940912136

1. 什么是迭代器?

概念(维基百科):迭代器(iterator),是确使用户可在容器对象(container,例如链表或数组)上遍访的对象[1][2][3],设计人员使用此接口无需关心容器对象的内存分配的实现细节。

JS中的迭代器

  • 其本质就是一个对象,符合迭代器协议(iterator protocol)

  • 迭代器协议

  1. 其对象返回一个next函数

  2. 调用next函数返回一个对象,其对象中包含两个属性

    • done(完成),它的值为布尔类型,也就是true/false

      • 如果这个迭代器没有迭代完成即返回{done:false}
      • 当这个迭代器完成了即返回{done:true}
    • value(值),它可以返回js中的任何值,TS中表示可为:value:any类型

1.1 迭代器的基本实现

思考以下代码:

let index = 0
const bears = ['ice''panda''grizzly']

let iterator = {
  next() {
    if (index < bears.length) {
      return { donefalsevalue: bears[index++] }
    }

    return { donetruevalueundefined }
  }
}

console.log(iterator.next()) //{ done: false, value: 'ice' }
console.log(iterator.next()) //{ done: false, value: 'panda' }
console.log(iterator.next()) //{ done: false, value: 'grizzly' }
console.log(iterator.next()) //{ done: true, value: undefined }

  1. 是一个对象,实现了next方法,next方法返回了一个对象,有done属性和value属性,且key的值类型也为booleanany,符合迭代器协议,是一个妥妥的迭代器没跑了。
  2. 弊端
  • 违背了高内聚思想,明明indexiterator对象是属于一个整体,我却使用了全局变量,从V8引擎的GC,可达性(也就是标记清除)来看,如果bears = null ,不手动设置为null很有可能会造成内存泄漏,并且内聚性低。
  • 假如我要创建一百个迭代器对象呢?那我就自己定义一百遍吗?肯定错误的,我们要把它封装起来,这样内聚性又高,又能进行复用,一举两得,一石二鸟,真的是very beautiful,very 优雅。

1.2 迭代器的封装实现

思考一下代码:

const bears = ['ice''panda''grizzly']

function createArrIterator(arr) {
  let index = 0

  let _iterator = {
    next() {
      if (index < arr.length) {
        return { donefalsevalue: arr[index++] }
      }

      return { donetruevalueundefined }
    }
  }

  return _iterator
}

let iter = createArrIterator(bears)

console.log(iter.next())
console.log(iter.next())
console.log(iter.next())
console.log(iter.next())
  • 内聚性非常高,尽最大可能进行了复用,减少冗余代码

2. 什么是可迭代对象

迭代器对象和可迭代对象是一个不同的东西,虽然它们存在关联,而且面试的时候经常面这些概念,废话不多说,我们直接进入主题。

  • 首先就是一个对象,且符合可迭代对象协议(iterable protocol)
  • 可迭代对象协议
  1. 实现了[Symbol.iterator]为key的方法,且这个方法返回了一个迭代器对象
  • 绕了一大圈终于把概念搞明白了,那可迭代对象有什么好处呢?有什么应用场景呢?
    1. for of 的时候,其本质就是调用的这个函数,也就是[Symbol.iterator]为key的方法

    2.1 原生可迭代对象(JS内置)

    1. String
    2. Array
    3. Set
    4. NodeList 类数组对象
    5. Arguments 类数组对象
    6. Map

    2.1.1 部分for of 演示

    let str = 'The Three Bears'

    const bears = ['ice''panda''grizzly']

    forlet text of str) {
      console.log(text) //字符串每个遍历打印
    }

    forlet bear of bears) {
      console.log(bear)
    }

     //ice panda grizzly

    2.1.2 查看内置的[Symbol.iterator]方法

    • 上面给大家举例了很多可迭代对象,那它们必定是符合可迭代对象协议的,思考以下代码
    const bears = ['ice''panda''grizzly']
    //数组的Symbol.iterator方法
    const iter = bears[Symbol.iterator]()

    console.log(iter.next())
    console.log(iter.next())
    console.log(iter.next())
    console.log(iter.next())


    const nickName = 'ice'
    //字符串的Symbol.iterator方法
    const strIter = nickName[Symbol.iterator]()

    console.log(strIter.next())
    console.log(strIter.next())
    console.log(strIter.next())
    console.log(strIter.next())

    2.2 可迭代对象的实现

    let info = {
      bears: ['ice''panda''grizzly'],
      [Symbol.iterator]: function() {
        let index = 0
        let _iterator = {
           //这里一定要箭头函数,或者手动保存上层作用域的this
           next() => {
            if (index < this.bears.length) {
              return { donefalsevaluethis.bears[index++] }
            }
      
            return { donetruevalueundefined }
          }
        }

        return _iterator
      }
    }

    let iter = info[Symbol.iterator]()
    console.log(iter.next())
    console.log(iter.next())
    console.log(iter.next())
    console.log(iter.next())

    //符合可迭代对象协议 就可以利用 for of 遍历
    for (let bear of info) {
      console.log(bear)
    }
    //ice panda grizzly
    • 符合可迭代对象协议,是一个对象,有[Symbol.iterator]方法,并且这个方法返回了一个迭代器对象。
    • 当我利用for of 遍历,就会自动的调用这个方法。

    2.3 可迭代对象的应用

    • for of
    • 展开语法
    • 解构语法
    • promise.all(iterable)
    • promise.race(iterable)
    • Array.from(iterable)
    • ...

    2.4 自定义类迭代实现

    class myInfo {
      constructor(name, age, friends) {
        this.name = name
        this.age = age
        this.friends = friends
      }

      [Symbol.iterator]() {
        let index = 0

        let _iterator = {
          next() => {
            const friends = this.friends
            if (index < friends.length) {
              return {donefalsevalue: friends[index++]}
            }

            return {donetruevalueundefined}
          }
        }

        return _iterator
      }
    }

    const info = new myInfo('ice'22, ['panda','grizzly'])

    for (let bear of info) {
      console.log(bear)
    }

    //panda
    //grizzly

    • 此案例只是简单的对friends进行了迭代,你也可以迭代你想要的一切东西...

    3. 生成器函数

    生成器是ES6新增的一种可以对函数控制的方案,能灵活的控制函数的暂停执行,继续执行等。

    生成器函数和普通函数的不同

    • 定义: 普通函数function定义,生成器函数function*,要在后面加*
    • 生成器函数可以通过 yield 来控制函数的执行
    • 生成器函数返回一个生成器(generator),生成器是一个特殊的迭代器

    3.1 生成器函数基本实现

    function* bar() {
      console.log('fn run')
    }

    bar()
    • 我们会发现,这个函数竟然没有执行。我们前面说过,它是一个生成器函数,它的返回值是一个生成器,同时也是一个特殊的迭代器,所以跟普通函数相比,好像暂停了,那如何让他执行呢?接下来我们进一步探讨。

    3.2 生成器函数单次执行

    function* bar() {
      console.log('fn run')
    }

    const generator = bar()

    console.log(generator.next())
    //fn run
    //{ value: undefined, done: true }
    • 返回了一个生成器,我们调用next方法就可以让函数执行,并且next方法是有返回值的,我们上面讲迭代器的时候有探讨过,而value没有返回值那就是undefined。那上面说的yield关键字在哪,到底是如何控制函数的呢?是如何用的呢?

    3.3 生成器函数多次执行

    function* bar() {
      console.log('fn run start')
      yield 100
      console.log('fn run...')
      yield 200
      console.log('fn run end')
      return 300
    }

    const generator = bar()

    //1. 执行到第一个yield,暂停之后,并且把yield的返回值 传入到value中
    console.log(generator.next())
    //2. 执行到第一个yield,暂停之后,并且把yield的返回值 传入到value中
    console.log(generator.next())
    //3. 执行剩余代码
    console.log(generator.next())

    //打印结果:
    //fn run start
    //{done:false, value: 100}
    //fn run...
    //{done:false, value: 200}
    //fn run end
    //{done:true, value: 300}
    • 现在我们恍然大悟,每当调用next方法的时候,代码就会开始执行,执行到yield x,后就会暂停,等待下一次调用next继续往下执行,周而复始,没有了yield关键字,进行最后一次next调用返回done:true

    3.4 生成器函数的分段传参

    我有一个需求,既然生成器能控制函数分段执行,我要你实现一个分段传参。

    思考以下代码:

    function* bar(nickName) {
      const str1 = yield nickName
      const str2 = yield str1 + nickName

      return str2 + str1 + nickName
    }

    const generator = bar('ice')

    console.log(generator.next())
    console.log(generator.next('panda '))
    console.log(generator.next('grizzly '))
    console.log(generator.next())

    // { value: 'ice', done: false }
    // { value: 'panda ice', done: false }
    // { value: 'grizzly panda ice', done: true }
    // { value: undefined, done: true }

    • 如果没有接触过这样的代码会比较奇怪
      • 当我调用next函数的时候,yield的左侧是可以接受参数的,也并不是所有的next方法的实参都能传递到生成器函数内部
      • yield左侧接收的,是第二次调用next传入的实参,那第一次传入的就没有yield关键字接收,所有只有当我调用bar函数的时候传入。
      • 最后一次next调用,传入的参数我也调用不了,因为没有yield关键字可以接收了。
    • 很多开发者会疑惑,这样写有什么用呢?可读性还差,但是在处理异步数据的时候就非常有用了。

    3.5 生成器代替迭代器

    前面我们讲到,生成器是一个特殊的迭代器,那生成器必定是可以代替迭代器对象的,思考以下代码。

    let bears = ['ice','panda','grizzly']

    function* createArrIterator(bears) {
      for (let bear of bears) {
        yield bear
      }
    }

    const generator = createArrIterator(bears)

    console.log(generator.next())
    console.log(generator.next())
    console.log(generator.next())
    console.log(generator.next())

    其实这里还有一种语法糖的写法yield*

    • yield* 依次迭代这个可迭代对象,相当于遍历拿出每一项 yield item(伪代码)

    思考以下代码:

    let bears = ['ice','panda','grizzly']

    function* createArrIterator(bears) {
      yield* bears
    }

    const generator = createArrIterator(bears)

    console.log(generator.next())
    console.log(generator.next())
    console.log(generator.next())
    console.log(generator.next())
    • 依次迭代这个可迭代对象,返回每个item值

    4. 可迭代对象的终极封装

    class myInfo {
      constructor(name, age, friends) {
        this.name = name
        this.age = age
        this.friends = friends
      }

      *[Symbol.iterator]() {
        yieldthis.friends
      }
    }

    const info = new myInfo('ice'22, ['panda','grizzly'])

    for (let bear of info) {
      console.log(bear)
    }

    //panda
    //grizzly
    • 回顾以下可迭代对象协议
      • 是一个对象并且有[Symbol.iterator]方法
      • 这个方法返回一个迭代器对象 生成器函数返回一个生成器,是一个特殊的迭代器

    5. 总结

    5.1 迭代器对象

    1. 本质就是一个对象,要符合迭代器协议
    2. 有自己对应的next方法,next方法则返回一组数据{done:boolean, value:any}

    5.2 可迭代对象

    1. 本质就是对象,要符合可迭代对象协议
    2. [Symbol.iterator]方法,并且调用这个方法返回一个迭代器

    5.3 生成器函数

    1. 可以控制函数的暂停执行和继续执行
    2. 通过function* bar() {} 这种形式定义
    3. 不会立马执行,而是返回一个生成器,生成器是一个特殊的迭代器对象
    4. yield 关键字可以控制函数分段执行
    5. 调用返回生成器的next方法进行执行


    - EOF -

    推荐阅读  点击标题可跳转

    1、一文吃透React v18全部Api(1.3w字)

    2、我相信这是 Vue3 复用代码的正确姿势!

    3、迄今为止最全的前端监控体系搭建篇(长文预警)


    觉得本文对你有帮助?请分享给更多人

    关注「大前端技术之路」加星标,提升前端技能

    点赞和在看就是最大的支持❤️

    继续滑动看下一个
    大前端技术之路
    向上滑动看下一个

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存