当我在数小黄鸭的时候,学长让我给他讲一讲JS中的迭代
我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第2篇文章,点击查看活动详情
学长:你这是不是一种迭代鸭子的行为。
我:emmm可能算是吧。
学长:那你给我讲一讲在JS中的迭代吧
这次我没有直接逃走,而是走到了学姐身后,哭诉学长对自己的打压(痛苦流涕),于是乎学长掏出了红宝书阴森森地笑道:你学不学!
话不多说,本文将由浅入深地从基础与原理再到实战与手撕展开,探究JS迭代的原理,解析官方文档的案例
迭代的基础
举一个经典且刻在印象中的迭代例子:对数组的元素按序访问,当遍历次数到达数组长度时停止迭代
let arr = [1,2,3]
for(let i = 0; i < arr.length; i++) {
console.log(arr[i])
}
上面我标注了按序访问与停止迭代,思考一下,得出迭代必须满足如下两点
- 有某种固有的顺序,可以按某种顺序开始或者停止迭代。例如:在数组中顺序对应着下标,遍历次数到达数组长度则停止
- 了解如何访问迭代目标的元素。例如:在数组中可以通过下标访问对应的元素
迭代的原理
好,上面我们已经掌握迭代的条件,那么将其带入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
不是可迭代对象,那么就是可以理解为,map
,set
,arr
等都是可迭代对象,这里可能有人想到那obj
对象类型可以使用for in
进行迭代,为什么不可以叫做可迭代对象呢?其是因为内部的实现机制与其他数据结果的迭代不同,这点我们放在实战部分聊。
上面这段话我标注了可迭代对象这一概念,而可迭代对象是什么呢?指的是map
,set
,str
这些数据结果吗?其实不然,官方文档针对可迭代对象给出了明确的要求:
- 实现正式的
interable
接口 - 可以通过
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.iterator
即interable
接口
继承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}
手动迭代的过程是这样的
- 通过
str[Symbol.iterator]()
返回拿到迭代器 - 调用迭代器对象的
next()
方法 - 返回一个IteratorResult 对象比如
{value: '猪', done: false}
- 该对象内包含了两种属性对应着不同的作用
value
代表当前迭代的内容,done
标记是否可以进行下一次next()
- 若
done
为true
则不会进行下一次迭代,说明迭代终止
迭代内部的机制我们已经学习完毕,下面开始练习时间,来看看常用的迭代的实战场景
迭代的实战
我们知道很多迭代的操作,比如最经典的for
,还有forEach
,for of
,for in
等等,那么同样是迭代,我们该如何选择正确的迭代方式呢?下面来一一分析
forEach
场景
forEach
方法常见的可以遍历对象有Set
,Map
,array
,NodeList
,它会迭代每个数据结构知道终止,因为无法在内部使用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
-
创建实例的时候传入
limit
给予实例长度,其实这里可以理解为我们在初始化数组的时候给与数组长度 -
进行
for of
操作的时候对应内部Symbol.iterator
方法并调用,返回一个迭代器对象,该对象包含一个next
方法// 内部机制 counter[Symbol.iterator]() // { // {next: ƒ} // }
-
调用
next
方法,返回{ done: false, value: count++ }
格式数据,每次调用为count
和limit
做判断,如果超过范围,那么将返回的对象的done
属性标记为false
说明迭代完毕
最后
经过学长的鞭策与学姐的鼓励,我又一次地领悟了迭代器地奥妙,整理并输出了该篇文章,如果对你有帮助就点小爱心吧!
转载自:https://juejin.cn/post/7147464367301197855