在上一篇文章中,我们初步了解到迭代是对具有迭代器对象内部元素的顺序访问。可迭代性由是否实现 Symbol.iterator
接口决定,与具体数据结构无关 —— 我们可以通过为对象添加迭代器使其可迭代,也能通过移除迭代器让数组等结构不可迭代✅。
本文将深入迭代器的内部实现,并手把手教你手写一个自定义迭代器。如果你对迭代的基础知识仍有困惑,强烈推荐先阅读本系列第一篇博文:
一、可迭代性与数据类型无关
突然想起高中语文课本中的一句文言文,恰能类比本节核心思想:
原文:
君子生非异也,善假于物也。
译文:君子并不是生来就与众不同,只是因为他们善于借助其他东西罢了。
对应本节中,可迭代性并非数组、字符串等数据类型的「原生属性」,而是通过Symbol.iterator
方法返回的迭代器实现的✅
我们把数组的迭代器去掉,它一样不可以使用 for…of 去遍历:
const arr = [1, 2, 3, 4, 5]
// ❌去掉 arr 的迭代器
arr[Symbol.iterator] = null
for(let val of arr) {
console.log(val) // 报错 arr 不是一个可迭代对象!
}
同理,我们也可以为普通对象添加迭代器使其支持for...of
,具体实现可见上篇文章示例。
二、迭代器的使用效果
既然要实现一个自定义的迭代器,我们不妨先看看原生的迭代器使用起来是什么效果。
const arr = [1, 2, 3, 4, 5]
// 获取 arr 的迭代器
const iter = arr[Symbol.iterator]()
console.log(iter.next()) // { value: 1, done: false }
console.log(iter.next()) // { value: 2, done: false }
console.log(iter.next()) // { value: 3, done: false }
console.log(iter.next()) // { value: 4, done: false }
console.log(iter.next()) // { value: 5, done: false }
console.log(iter.next()) // { value: undefined, done: true }
- 调用
Symbol.iterator
会返回一个迭代器对象,该对象包含next()
方法; - 每次调用
next()
返回一个包含value
(当前元素)和done
(是否结束)的对象; - 当
done: true
时,value
可为任意值(规范未强制要求,通常为undefined
)。
由此我们可以整理出如下的结构伪代码:
// 解构伪代码
arr {
... 其他属性
Symbol.iterator: function() {
return {
next: function () {
return { value, done }
}
}
}
}
下面,我们就从上面效果整理出的结构为起点,反推迭代器的实现。
三、从效果反推实现
根据上面的结构,我们知道,arr上面有一个属性 Symbol.iterator,这个属性对应一个方法,将返回一个带有next函数的对象:
const arr = [1, 2, 3, 4, 5]
arr[Symbol.iterator] = function () {
return {
next: function () {
}
}
}
我们需要一个指针指示当前被返回的元素,这个指针默认指向第一个元素。并且多次调用 next 方法时,该指针并没有被重置,而是基于目前位置继续向前移动,可以基本断定我们需要用到闭包
来保存多次调用时对指针的引用。
同时,由于我们需要访问 arr 中的元素,但是 next 函数作用域中获取不到 arr,我们需要在 return 之前保留当前作用域的 this:
- 使用闭包保存迭代指针
i
,避免多次调用next()
时指针重置; - 通过
this
引用保存原数组,确保在闭包中能访问数组元素。
const arr = [1, 2, 3, 4, 5]
arr[Symbol.iterator] = function () {
let i = 0 // 指针指向第一个元素
const arrRef = this // 保存当前作用域 this
return {
next: function () {
// 代码中使用 i,保存对 i 的引用,形成闭包
}
}
}
接下来就很简单了,每次调用 next 时,我们需要判断指针是否已经完成了迭代,如果已经完成就返回{ value: undefined, done: true }
,没有完成返回 { value: arr[i], done: false }
arr[Symbol.iterator] = function () {
let i = 0
const arrRef = this
return {
next: function() {
if (i >= arrRef.length) {
return { value: undefined, done: true } // 迭代结束
} else {
const value = arrRef[i]
i += 1 // 指针前进
return { value, done: false } // 迭代未结束
}
}
}
}
到此为止,我们就简单地复现了数组迭代器的内部实现,让我们看看运行出的效果和原生迭代器是否一致吧:
const arr = [1, 2, 3, 4, 5]
// 我们的迭代器将覆盖原有的迭代器
arr[Symbol.iterator] = function () {
let i = 0
const arrRef = this
return {
next: function() {
if (i >= arrRef.length) {
return { value: undefined, done: true }
} else {
const value = arrRef[i]
i += 1
return { value, done: false }
}
}
}
}
const iter = arr[Symbol.iterator]()
console.log(iter.next()) // { value: 1, done: false }
console.log(iter.next()) // { value: 2, done: false }
console.log(iter.next()) // { value: 3, done: false }
console.log(iter.next()) // { value: 4, done: false }
console.log(iter.next()) // { value: 5, done: false }
console.log(iter.next()) // { value: undefined, done: true }
运行结果完全一致!
四、自定义一个简单的迭代器
让我们实现自定义迭代器,让数组通过for...of
遍历时仅输出奇数索引元素(1, 3, 5),你知道怎么实现了吗?
const arr = [1, 2, 3, 4, 5]
for(let val of arr) {
console.log(val) // 要求打印 1, 3, 5
}
很简单,上面在实现迭代器的代码中,每次我们让指针 i 前进一步来迭代每一个元素。现在我们只需要让指针 i 每次前进两步就可以啦:
arr[Symbol.iterator] = function () {
let i = 0
const arrRef = this
return {
next: function() {
if (i >= arrRef.length) {
return { value: undefined, done: true }
} else {
const value = arrRef[i]
i += 2 // 指针前进两步,跳过奇数索引元素
return { value, done: false }
}
}
}
}
for(let val of arr) {
console.log(val) // 打印 1, 3, 5
}
是不是很神奇呢?这也正说明 for … of 确实是基于迭代实现的遍历。
五、总结
我们基于上篇文章的迭代的理解,在本篇文章中继续深入理解迭代的实现原理,从原生的实现效果入手确定代码结构骨架,然后一点点根据目标效果对代码进行补全,最后实现了一个自定义的迭代器使 for…of的行为发生变化。
当然,回归到我们的面试题上,题目中除了对于迭代的考察之外,还似乎在有意考察数组解构
和对象解构
的区别。
事实上虽然同为解构,但是两者的实现原理可谓大相径庭。
延伸问题(后续博文预告)
- 数组解构 vs 对象解构:两者的底层实现有何本质区别?
- 生成器函数(Generator) :如何通过更简洁的语法创建迭代器?
- 实际应用场景:迭代器在异步编程、数据流处理中有哪些经典用法?
接下来的文章将围绕这些问题展开,欢迎持续关注!✨