likes
comments
collection
share

当我在数小黄鸭的时候,学长让我给他讲一讲JS中的迭代

作者站长头像
站长
· 阅读数 79

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第2篇文章,点击查看活动详情

学长:你这是不是一种迭代鸭子的行为。

我:emmm可能算是吧。

学长:那你给我讲一讲在JS中的迭代吧

这次我没有直接逃走,而是走到了学姐身后,哭诉学长对自己的打压(痛苦流涕),于是乎学长掏出了红宝书阴森森地笑道:你学不学!

话不多说,本文将由浅入深地从基础与原理再到实战与手撕展开,探究JS迭代的原理,解析官方文档的案例

迭代的基础

举一个经典且刻在印象中的迭代例子:对数组的元素按序访问,当遍历次数到达数组长度时停止迭代

let arr = [1,2,3]
for(let i = 0; i < arr.length; i++) {
    console.log(arr[i])
}

上面我标注了按序访问停止迭代,思考一下,得出迭代必须满足如下两点

  1. 有某种固有的顺序,可以按某种顺序开始或者停止迭代。例如:在数组中顺序对应着下标,遍历次数到达数组长度则停止
  2. 了解如何访问迭代目标的元素。例如:在数组中可以通过下标访问对应的元素

迭代的原理

好,上面我们已经掌握迭代的条件,那么将其带入Javascript中,分析一下其他可迭代的数据结构的迭代内部原理是怎么样的。

可迭代的数据结构

我们使用for of来对JS中常见的数据结构进行迭代,结果如下

let str = "猪痞恶霸"  // 猪 痞 恶 霸
let arr = [1] // 1
let obj = {
    name:"猪痞恶霸" 
} // obj is not iterable
let m = new Map() // ['猪痞恶霸', 1]
m.set(str,1)
let s = new Set([1]) // 1
for (item of str) {
    console.log(item)
}

这里看到只有obj抛出了错误:obj is not iterable,翻译为obj不是可迭代对象,那么就是可以理解为,mapsetarr等都是可迭代对象,这里可能有人想到那obj对象类型可以使用for in进行迭代,为什么不可以叫做可迭代对象呢?其是因为内部的实现机制与其他数据结果的迭代不同,这点我们放在实战部分聊。

上面这段话我标注了可迭代对象这一概念,而可迭代对象是什么呢?指的是mapsetstr这些数据结果吗?其实不然,官方文档针对可迭代对象给出了明确的要求:

  1. 实现正式的interable接口
  2. 可以通过interator迭代器消费

这是啥啊,看不明白,那么我们下面来深入解析这两点要求。

interable接口

interable接口又称可迭代协议是一个比较抽象的东西,我将它理解为数据结构迭代操作的入口,不同的数据结构通过该接口可以实现相同的操作,而在javascript中正是利用Symbol.iterator来作为这个接口,如果不了解Symbol的掘友可以看一下这篇文章:学长突然问我用过Symbol吗,我哽咽住了(准备挨骂)比较细致地说明了Symbol的一些应用。

如果你了解Symbol那么肯定知道内置Symbol是什么,Symbol.iterator正是一种内置Symbol,它对应的外部操作即是for of,当外部使用for of方法时,内部就会启用以Symbol.iterator为键的方法,下面我们通过代码来分析内部的构造

let arr = [1,2,3]
console.log(arr[Symbol.iterator]) //  values() { [native code] }

values() { [native code] }正是Symbol.iterator对应的函数,而只有相关类型内置了Symbol.iterator才会打印出来

let obj = {}
console.log(obj[Symbol.iterator]) // undefined

如上例子obj[Symbol.iterator]undefined,这也是为什么说obj无法进行for of进行迭代,因为其没有内置Symbol.iteratorinterable接口

继承interable接口

interable接口是可以被继承的,也就是说当父类实现了interable接口,那么子类也会实现

class Son extends String {}
let son = new Son()
console.log(son[Symbol.iterator]) // [Symbol.iterator]() { [native code] }

我们通过上面的代码可以看到Son继承String类,所以内部也实现了interable接口

非常好,我们通过上面的两点已经了解了可迭代对象的第一个要求:实现正式的interable接口,那么interator迭代器又是什么呢?

interator迭代器

interator迭代器本质上是对象,由interable接口对应的函数执行生成

let arr = [1,2,3]
console.log(arr[Symbol.iterator]()) // Array Iterator {}            

上面调用了arr[Symbol.iterator]()方法获得了一个Array Iterator {}对象,也就是迭代器,其实这种方法是手动获取的,真正迭代时的内部操作。

interator迭代器是怎么做到迭代数据结构的呢?他的核心就是使用next(),下面我们手动迭代一个字符串

let str = "猪痞恶霸"
let strInter = str[Symbol.iterator]()
strInter.next() // {value: '猪', done: false}
strInter.next() // {value: '痞', done: false}

手动迭代的过程是这样的

  1. 通过str[Symbol.iterator]()返回拿到迭代器
  2. 调用迭代器对象的next()方法
  3. 返回一个IteratorResult 对象比如{value: '猪', done: false}
  4. 该对象内包含了两种属性对应着不同的作用
  5. value代表当前迭代的内容,done标记是否可以进行下一次next()
  6. donetrue则不会进行下一次迭代,说明迭代终止

迭代内部的机制我们已经学习完毕,下面开始练习时间,来看看常用的迭代的实战场景

迭代的实战

我们知道很多迭代的操作,比如最经典的for,还有forEachfor offor in等等,那么同样是迭代,我们该如何选择正确的迭代方式呢?下面来一一分析

forEach场景

forEach方法常见的可以遍历对象有SetMaparrayNodeList,它会迭代每个数据结构知道终止,因为无法在内部使用return或者是break所以无法终止迭代,所以说如果我们的迭代无需终止,就可以使用forEach

let arr = [
    {name:"猪痞恶霸",age:20},
    {name:"Ned",age:21}
]
arr.forEach((item) => {
    item.age+=1
})

比如使用forEach来对数组元素进行批量处理

for of场景

上面提到了forEach的迭代是无法终止的,所以当我们想终止迭代,那么我们就可以使用for of,比如在当元素满足某个条件的时候终止迭代

let arr = [1,3,10,7,10]
for(item of arr) {
    console.log(item)
    if(item%2 === 0) {
        break
    }
} 
// 1,3,10

如上,当元素是偶数的时候那么就停止迭代,相对于forEach来看for of有个缺陷就是无法获取当前元素对应的索引,而forEach可以,所以我们需要选择不同的迭代操作来适应当前的需求。

for in场景

由于Object没有内置迭代器,所以for of无法对其进行迭代,我们可以使用for in方法来迭代对象,其返回的是属性的键名

let obj = {
    name:"ned",
    like:"man"
}
for(item in obj) {
    console.log(item)
} // name like

当然如果你想要针对对象进行一些特性地迭代,比如迭代Symbol属性,那么可以参考一下这篇文章:JS遍历对象的七种方法

Very Good!!!通过上面的三种迭代场景,我们学到了可以根据不同类型的迭代对象,参数需要,以及性能相关来判断使用哪种迭代操作,那么下面我给大家带来点花的:手撕迭代

手撕迭代

来源:JS高程4,下面我会根据这个手撕来给大家分析,帮助大家进一步透彻迭代

class Counter {
  constructor(limit) {
    this.limit = limit;
  }
  [Symbol.iterator]() {
    let count = 1,
      limit = this.limit;
    return {
      next() {
        if (count <= limit) {
          return { done: false, value: count++ };
        } else {
          return { done: true, value: undefined };
        }
      },
    };
  }
}
let counter = new Counter(3);
for (let i of counter) { console.log(i); } 

这是一个实现迭代的类Counter,内部有构造函数与迭代器接口Symbol.iterator

  1. 创建实例的时候传入limit给予实例长度,其实这里可以理解为我们在初始化数组的时候给与数组长度

  2. 进行for of操作的时候对应内部Symbol.iterator方法并调用,返回一个迭代器对象,该对象包含一个next方法

    // 内部机制
    counter[Symbol.iterator]()
    // {
    //  {next: ƒ}
    // }
    
  3. 调用next方法,返回{ done: false, value: count++ }格式数据,每次调用为countlimit做判断,如果超过范围,那么将返回的对象的done属性标记为false说明迭代完毕

最后

经过学长的鞭策与学姐的鼓励,我又一次地领悟了迭代器地奥妙,整理并输出了该篇文章,如果对你有帮助就点小爱心吧!

转载自:https://juejin.cn/post/7147464367301197855
评论
请登录