引言
提到迭代器与可迭代对象,很多人的第一反应就会想到 for...of ,在日常编程中,我们通常喜欢使用 for...of 去迭代一个数组或者字符串等来获取我们想要的内容。但也因为经常使用,我们往往忽略了它的运行原理,本篇文章将从什么是迭代器,如何创建迭代器,什么是可迭代对象,如何创建可迭代对象,如何把一个不能迭代的对象变的可以迭代,什么是迭代器协议,以及他们的一些特点来详细介绍。
什么是迭代器(iterator)
了解 for ... of
相信很多人会有这样一个疑惑:我经常使用 for...of 去迭代某个值,并且经常听别人说迭代器的时候也会使用 for...of ,那是不是可以理解为,我所用的 for...of 就是人们常说的迭代器呢?
事实上,for...of 其实更像是一种语法糖,它的作用是将可迭代对象的遍历过程变得更加的简洁和直观,本质上来看,当我们使用了 for ..of 的时候,其实代码进行了下面的操作:
(注:在阅读下块内容的时候,如果出现不理解的地方不用着急,简单看一下即可,详细内容会在文章下面的内容中提到,主要目的在于理解 for...of 是专门用于处理迭代器的一种循环结构)
for...of 做了什么
这里会简单表述一下它的执行过程,文章后面会给出详细介绍:
- 首先,检查要迭代的对象是否具有一个名为 Symbol.iterator 的属性,如果对象没有这个属性,或者属性的值不是函数,那么 for...of 循环将无法进行,会抛出一个错误:*** 不是一个可迭代对象。
- 如果对象具有 Symbol.iterator 属性并且其值是一个函数,for..of 会循环调用这个函数来获取一个对象(其实这个对象就是一个迭代器,后面会提到)。
- 在循环调用这个对象的时候,这个对象内部的一个属性会把它想要给你的值传递出去,也就是我们拿到的结果,并且告诉 for...of 是否迭代结束。
- 当 for...of 收到迭代结束的消息的时候,它就会停止。
以下是代码示例:
const arr = [1, 2, 3]; // 这是一个订阅好数组
const iterator = arr[Symbol.iterator](); // 获取迭代器对象
let iteration = iterator.next();
// 进行第一次迭代,每次迭代得到一个对象,对象内部有属性可以判断是否迭代结束以及这个值
while (!iteration.done) { // 通过done来控制迭代过程什么时候结束(下面会提到)
const value = iteration.value; // 拿到迭代过程得到的值
console.log(value);
iteration = iterator.next();
// 将这个值重新赋值,迭代器内部会有换到下一个值的逻辑
}
就像上述代码所能得到的,for...of 不是迭代器,它是基于迭代器进行的一系列操作上的简洁的写法,像是一种语法糖,有效的减少了我们迭代一个对象的过程的代码量(不然我们就需要手动写类似于上述代码的迭代过程)。
迭代器的定义
在上述内容中, 我们知道了 for...of 操作的是一个迭代器,并且有意思的是,迭代器是一个对象,在上述的 for...of 的执行过程中,这个迭代器不但告诉了我们迭代是否结束,也告诉我们获取的值,从它的行为上来看,它就像是一个非常好用的工具一样。
我们先来看看官方的定义:
迭代器(iterator),是确使用户可在容器物件(container,例如链表或数组)上遍访的物件,设计人员使用此接口无需关心容器物件的内存分配的实现细节。其行为很像数据库技术中的光标(cursor),迭代器最早出现在1974年设计的CLU编程语言中。
结合官方的话语,以及上述描述的内容,用通俗点的话语来总结,在JavaScript中,迭代器其实就是一个用来遍历容器的对象!
迭代器的规范
在JavaScript中,迭代器是有规范的,并且这个规范非常严格,如果你要创建一个迭代器,尽量严格遵循迭代器协议。
迭代器协议的主要组成部分如下:
-
Symbol.iterator:这是一个由 JavaScript 内置的
Symbol
类型创建的特殊属性,它是一个无参数函数,用于返回一个迭代器对象。如果一个对象具有Symbol.iterator
属性并且其值是一个函数,那么这个对象就可以被视为可迭代对象。 -
next() 方法:迭代器对象必须实现一个
next()
方法,该方法没有参数。每次调用next()
方法,它会返回一个包含两个属性的对象:value
和done
。value
表示当前迭代到的值,done
是一个布尔值,表示迭代是否结束(是的,就是它在控制 for...of 什么时候结束)。当迭代结束时,done
应该为true
,并且value
可以是任意值。
如果你遵循了上述规则,其实就可以创建自己的迭代器了,现在来从简单的思路来写,层层递进:
创建一个简单的迭代器
首先,假设我们有一个数组:
const names = ["路人甲", "路人乙", "路人丙"]
我们从简单的地方开始,把上面这个数组看做一个容器,现在先创建一个用于迭代这个数组的迭代器,已知这是一个对象:
const namesIterator = {}
它内部有一个next属性,这个属性是一个方法,方法会返回一个包含两个属性的对象,两个属性分别为 done 和 value, 其中 done 的值是一个boolean类型,用来决定是否遍历结束:
const namesIterator = {
next: function() {
return {done: false, value: "一个值"} // done为false时候,表示还没有迭代结束
}
}
是的,上述就是一个 Symbol.iterator 要返回的迭代器对象的大致原型,接下来我们添加一些逻辑上的内容:
const names = ["路人甲", "路人乙", "路人丙"]
// 首先,我们创建一个索引,用来遍历数组的值
let index = 0
// 接着,我们在上一步中的代码的基础上进行修改
const namesIterator = {
next: function() {
if (index < names.length) {
// 没有迭代结束时返回
return {done: false, value: names[index++]}
// index++:先使用这个index,再+1
} else {
// 迭代结束后返回
return {done: true}
}
}
}
然后可以发现,我们想要调用的时候可以如下操作:
const res1 = namesIterator.next() // 返回{done: false, value: "路人甲"}
const res2 = namesIterator.next() // 类似上述的返回
const res3 = namesIterator.next()
const res4 = namesIterator.next() // {done: true}
有没有发现什么?是的,在前面我们提到的 for...of 的执行过程中,其实写过上述的操作:
从外观上,我们的迭代器已经初步成型,但是还没有成型,因为还缺少一些必要的步骤,但是在进行剩下的步骤之前,我们需要先了解一下什么事可迭代对象,所以先 咔~!
可迭代对象(iterable)
我们已经大概知道迭代器对象是什么了,那么什么叫做可迭代对象呢?虽然我们会经常迭代原生的一些可迭代对象,比如Array,比如String,比如Map,比如Set等等等等好多好多,但是为什么他们是可迭代对象呢,为什么别的一些结构不是可迭代对象呢?为什么对象不是可迭代对象呢?
其实可迭代对象很好理解,在上述的对迭代器的逐步认识中,我们会发现,可迭代对象实质上是依靠的迭代器这个工具,就像上述我们写的一个迭代names的简单的迭代器,那你说,有没有一种可能,可迭代对象与不可迭代的对象之间的区别就在于它有没有这个工具呢?
没错!一个结构是不是可以迭代的对象,就在于它能不能拿出这个迭代器工具,就像是一个电影院,只有拿到票的人才能进入,如果没有票,对不起哦,爱答不理!
那问题就来了,如果一个没有电影票的人买了一张电影票,电影院还会让他进吗?当然会啦!他都有票了,为什么不让进呢?
那么,假设我有一个对象,这个对象本身不是一个可迭代对象,但是如果我给它写一个迭代器,那它也就可以迭代了!这是一个同样的道理!
所以我们可以知道,一个对象是不是一个可以迭代的对象,在于它内部是否有一个迭代器的存在,这样我们使用 for...of 的时候,for...of 就能拿到或者拿不到里面的迭代器去对这个对象进行迭代。
接着优化我们之前写的迭代器
之前写的迭代器是写在外面的,而且没有完成迭代器协议中的 Symbol.iterator 这一步,接下来,我们在此基础上继续完善:
首先,我们知道 for...of 会获取要迭代的对象中是否有 Symbol.iterator,在迭代器规范中,这个 Symbol.iterator 是一个函数并且会返回一个迭代器对象,而我们就顺这个这个要求来:
const names = ["路人甲", "路人乙", "路人丙"]
// 数组也是一种特殊的对象,我们直接给它添加一个叫Symbol.iterator的属性
names[Symbol.iterator] = function() {
let index = 0
const namesIterator = {
next: function() {
if (index < names.length) {
return {done: false, value: names[index++]}
} else {
return {done: true}
}
}
}
// 将迭代器写在这个方法内部并且能够返回
return namesIterator
}
for (var item of names) {
console.log(item)
} // 正确输出了路人甲,路人乙...
此时可能有人说,for...of 本来就能遍历这个数组,我怎么知道写对了没有,注意,for...of 也是先执行的 Symbol.iterator() 拿到的迭代器在进行操作,而我们写的这个Symbol.iterator 相当于写在对象的属性里,是优先被查找到的,我们可以做个实验:
const names = ["路人甲", "路人乙", "路人丙"]
names[Symbol.iterator] = function() {
let index = 0
const namesIterator = {
next: function() {
if (index < names.length) {
return {done: false, value: `第${index++}次迭代`} // 修改的这里
} else {
return {done: true}
}
}
}
return namesIterator
}
for (var item of names) {
console.log(item)
}
/**
第0次迭代
第1次迭代
第2次迭代
*/
是的,你会发现,for...of 迭代的是我们所写的迭代器,并且主要通过上述代码,我们知道了一件事:
迭代器拿到的数据其实是可以修改的,而默认的一些可迭代的原生数据结构,它们经过迭代器返回的值也是被写好的,如果你想改,是随时可以改的(重写)。
脱离掉一些局限性
上述代码成功实现了一个迭代器,但是我们发现,我们写的迭代器中,一直都在用names,这样会出现很大的局限性,因为它只能操作names这个数组,现在我想提高难度了,想要将一个对象变为可迭代对象,该怎么办?
首先,步骤是一样的,我们先把半成品写出来(不着急慢慢来):
// 创建一个对象
const obj = {
name: "meiciko",
age: 18,
height: 1.88
}
// 给对象写一个迭代器
obj[Symbol.iterator] = function() {
const keys = Object.keys(obj) // 获取对象的key组成的数组
let index = 0
const ObjIterator = {
next: function() {
if (index < keys.length) {
return {done: false, value: keys[index++]}
} else {
return {done: true}
}
}
}
return ObjIterator
}
for (var item of obj) {
console.log(item)
}
此时这个对象已经是一个可遍历的对象了,现在我们想提高这个迭代器的灵活性,肯定要把内部这个obj替换为this!
(注:下面这个思路需要一定的this和箭头函数基础)
但是直接替换有一个问题,在迭代器内部的next函数中,this指向的并不是这个对象,它指向的是ObjIterator这个迭代器对象,这时候就需要想办法,在使用this的时候,需要他跳过一层达到外面的一层,可以使用箭头函数,因为箭头函数是没有this的,它会使用上层作用域中的this,而上层作用域也就是[Symbol.iterator]这个函数,它指向的就是obj,所以我们可以这样修改:
const obj = {
name: "meiciko",
age: 18,
height: 1.88
}
obj[Symbol.iterator] = function() {
const keys = Object.keys(obj)
let index = 0
const ObjIterator = {
next: () => {
if (index < keys.length) {
return {done: false, value: keys[index++]}
} else {
return {done: true}
}
}
}
return ObjIterator
}
for (var item of obj) {
console.log(item)
}
更加的深入思考
如果我们需要我们想要某个类的对象,或者说我们所有使用的对象都变成一个可迭代的对象,难道我们要在每个对象中都创建一个类似上面的迭代器吗?这样做不但不合理,而且会占用大量的内存,所以,我们可以在某个类的原型中或者在Objecy的原型中修改,例如:
Object.prototype[Symbol.iterator] = function() {
let index = 0
const keys = Object.keys(this)
const ObjInterator = {
next: () => {
if (index < keys.length) {
return {done: false, value: keys[index++]}
} else {
return {done: true}
}
}
}
return ObjInterator
}
因为返回的值我们可以自己定义,假设我想要得到的是每个属性和对应的值组成的数组:
Object.prototype[Symbol.iterator] = function() {
let index = 0
const keys = Object.keys(this)
const ObjInterator = {
next: () => {
if (index < keys.length) {
index += 1
return {done: false, value: [keys[index - 1], this[keys[index -1]]]}
} else {
return {done: true}
}
}
}
return ObjInterator
}
const obj = {
name: "meiciko",
age: 18,
height: 1.88
}
for (var item of obj) {
console.log(item)
}
// 输出:
(2) ['name', 'meiciko']
(2) ['age', 18]
(2) ['height', 1.88]
是的,想怎么改怎么改!如果写在原型中,那么对于我们此时创建的对象来说,每个对象都是一个可迭代对象,而且我们可以想让它返回什么就返回什么。
应用场景
其实,关于上述的所有内容开发中用到的也是很少的,因为JavaScript本身提供的API已经足够优秀,而对于某个类的实例化对象,有时我们会想要拿到一些特殊的数据的时候,可以使用手写迭代器,只需要在class的类中写上一个叫 [Symbol.iterator] 的方法就好了(注意要把内部的迭代器返回)。