字节面试出过这样一道题, let [a, b] = { a: 1, b: 2 } ,在不改动源代码的基础上让这行代码成立,让a与b的值分别为1和2。这一眼看上去这道题太奇怪了,数组的解构我会,对象的解构我也会,让数组的解构等于一个对象,这是什么意思?
想要解决这道题,那就得来好好聊聊js中的迭代器了,在es6中很容易被人忽视的一个东西。
1. 迭代器
在 JavaScript 中,迭代器(Iterator) 是一种允许按顺序访问集合数据的机制,核心是通过 Symbol.iterator 方法实现。
以下是一些常见的迭代器:数组、字符串、Map、Set和类数组对象。对象并不是迭代器。
迭代器有些什么特性呢?在迭代器的原型上,有这样一个属性:Symbol.iterator,它是一个函数。我们可以拿数组来举个例子看看。
const arr = [1, 2, 3]
const iterator = arr[Symbol.iterator]()
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
我们调用数组的这个方法,可以得到一个迭代器对象,这个对象可以调用next方法,我们来看看结果会是什么。
每次调用next(),都会返回一个包含value和done属性的对象。当done为true时,迭代结束。和generator函数简直一模一样。
我们可以来手动实现一个数组的迭代器函数:
function createIterator(array) {
let index = 0
return {
next: function () {
if (index < array.length) {
return { value: array[index++], done: false }
} else {
return { value: undefined, done: true }
}
}
}
}
我们定义一个createIterator函数,它接收一个数组作为形参,用来实现数组迭代器的效果。
我们知道,调用Symbol.iterator属性会得到一个对象,这个对象要能够调用next方法。所以在我们自己定义的这个函数,我们返回一个对象,对象中有一个属性next,值为一个函数。每调用一次next函数会得到一个对象,利用index索引来获取数组的每一个值,当遍历完数组后让done为true。
我们来调用一下看看:
const myIterator = createIterator([1, 2, 3])
console.log(myIterator.next());// {value: 1, done: false}
console.log(myIterator.next());// {value: 2, done: false}
console.log(myIterator.next());// {value: 3, done: false}
console.log(myIterator.next());// {value: undefined, done: true}
成功的实现了和数组迭代器一样的效果。
那我们为什么要聊迭代器呢?这是因为数组解构需要右边是一个可迭代的对象,比如数组或者有Symbol.iterator方法的对象。所以要让这个赋值成立,需要让这个对象变得可迭代。
原来的代码let [a, b] = {a:1, b:2};会报错,因为对象没有默认的迭代器。要让其工作,必须给对象添加Symbol.iterator方法,使得在解构时能够按顺序返回值1和2。比如,这个迭代器应该依次返回1和2,然后done为true。
2. for...of 和 for...in 的区别
在聊怎么让对象变成迭代器之前,我们可以来聊聊for...of 和 for...in 的区别。我们已经知道,数组的解构需要右边是一个迭代器对象,这是因为数组在解构时就会调用这个Symbol.iterator 方法,返回一个生成器或迭代器对象,然后调用next方法来实现解构赋值。
而for...of的原理也是这样:
for...of:遍历可迭代对象的值,调用Symbol.iterator。
for...in:遍历对象的可枚举属性键名(包括原型链),适用于普通对象。
// for...of 遍历值(需要对象可迭代)
for (const val of obj) {
console.log(val); // 输出 1, 2
}
// for...in 遍历键名
for (const key in obj) {
console.log(key); // 输出 "a", "b"
}
当用for...of 去遍历一个迭代器对象时,for...of 就会去调用迭代器对象身上的Symbol.iterator 方法得到一个迭代器对象,然后去调用这个对象上的next方法来实现对迭代器对象的遍历。
3. 让 let [a, b] = {a:1, b:2} 成立
再回到我们的正题上来,所以想让 let [a, b] = {a:1, b:2} 这行代码成立,就得让右边的对象变成迭代器对象,那怎么让对象变成一个迭代器呢?
因为它题目要求我们不能动这行代码,所以我们可以在对象的原型上定义一个迭代器函数,当进行数组的解构时,就会去调用这个方法。
第一种方法:我们可以直接往对象的原型上挂一个generator生成器函数。
// 1. 为所有对象添加默认迭代器(按属性值顺序迭代)
Object.prototype[Symbol.iterator] = function* () {
const values = Object.values(this); // 获取对象的值数组
for (const val of values) {
yield val; // 按顺序返回属性值
}
};
// 2. 原代码不修改,直接运行
let [a, b] = {a:1, b:2};
console.log(a, b); // 输出 1 2
实现原理
-
全局迭代器扩展:
- 通过修改
Object.prototype的Symbol.iterator,所有对象都会继承这一迭代器。 Object.values(this)会提取对象的值,并按属性定义的顺序(ES6+ 中对象属性顺序是保留的)返回数组。
- 通过修改
-
解构赋值触发迭代:
let [a, b] = {a:1, b:2}会调用对象的Symbol.iterator,依次获取值1和2,完成解构。
第二种:我们可以利用数组的迭代器方法
Object.prototype[Symbol.iterator] = function () {
return Object.values(this)[Symbol.iterator]()
}
let [a, b] = { a: 1, b: 2 }
console.log(a, b);
关键原理
-
劫持对象默认迭代行为:
- 通过修改
Object.prototype的Symbol.iterator,所有对象都会继承这个方法。 - 当对象被迭代时(如解构赋值、
for...of),会自动调用Object.values(this)。
- 通过修改
-
Object.values()的作用:- 将对象的值按属性定义的顺序提取为数组。例如
{a:1, b:2}转为[1, 2]。 - 调用数组的
Symbol.iterator(即数组原生迭代器),返回一个按顺序遍历数组元素的迭代器。
- 将对象的值按属性定义的顺序提取为数组。例如
-
解构赋值的触发:
let [a, b] = {a:1, b:2}会尝试从右侧对象中提取迭代器。- 由于对象的
Symbol.iterator被修改为返回[1, 2]的数组迭代器,解构过程等同于let [a, b] = [1, 2]。
4. 总结
- 迭代器为对象提供了自定义遍历能力。
for...of依赖Symbol.iterator,而for...in遍历键名。- 对象解构赋值需要对象可迭代,通过实现
Symbol.iterator可达成目标。
JavaScript 的迭代器机制为数据遍历提供了统一接口,内置对象(数组、字符串等)已实现迭代器,普通对象可通过手动添加 Symbol.iterator 支持迭代。生成器函数是简化迭代器实现的利器,结合 for...of 或解构赋值,能更优雅地处理数据遍历。