用 let [a, b] = { a: 1, b: 2 } 看懂 js 中的迭代器

842 阅读5分钟

字节面试出过这样一道题, 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方法,我们来看看结果会是什么。

image.png

每次调用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}

image.png

成功的实现了和数组迭代器一样的效果。

那我们为什么要聊迭代器呢?这是因为数组解构需要右边是一个可迭代的对象,比如数组或者有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

实现原理

  1. 全局迭代器扩展

    • 通过修改 Object.prototype 的 Symbol.iterator,所有对象都会继承这一迭代器。
    • Object.values(this) 会提取对象的值,并按属性定义的顺序(ES6+ 中对象属性顺序是保留的)返回数组。
  2. 解构赋值触发迭代

    • 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);

关键原理

  1. 劫持对象默认迭代行为

    • 通过修改 Object.prototype 的 Symbol.iterator,所有对象都会继承这个方法。
    • 当对象被迭代时(如解构赋值、for...of),会自动调用 Object.values(this)
  2. Object.values() 的作用

    • 将对象的值按属性定义的顺序提取为数组。例如 {a:1, b:2} 转为 [1, 2]
    • 调用数组的 Symbol.iterator(即数组原生迭代器),返回一个按顺序遍历数组元素的迭代器。
  3. 解构赋值的触发

    • 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 或解构赋值,能更优雅地处理数据遍历。