JS迭代的魔力:让普通对象也拥有数组般的解构能力

141 阅读4分钟

一道解构的题目 ✨

在 JavaScript 中,我们经常使用数组的解构赋值来方便地提取元素:

const arr = [3, 4];
const [a, b] = arr;
console.log(a, b); // 3 4

这种简洁的语法背后,是数组内置的迭代器(Iterator)在起作用。一个对象如果拥有 [Symbol.iterator] 这个属性,并且该属性的值是一个返回迭代器对象的函数,那么它就是可迭代的(Iterable)。数组正是如此。

然而,对于普通的对象,例如 { a: 3, b: 4 },直接进行数组解构赋值是会报错的:

var [a, b] = {
  a: 3,
  b: 4,
};
console.log(a, b); // 报错 TypeError: {(intermediate value)(intermediate value)} is not iterable

错误信息清晰地告诉我们:这个对象不是可迭代的。 那么,有没有办法让普通对象也拥有像数组一样的迭代能力,从而实现解构赋值呢?

答案是肯定的!我们可以通过给 Object.prototype 添加一个 [Symbol.iterator] 属性来实现。

💡 让对象可迭代:改写 Object.prototype

Symbol.iterator 是一个内置的 Symbol,它作为一个属性名用于表示一个对象的默认迭代器。通过给 Object.prototype 添加这个属性,我们可以让所有普通对象都具备迭代的能力。

来看这段代码:

Object.prototype[Symbol.iterator] = function* () {
  yield* Object.values(this);
};

// 让下面的代码成立
var [a, b] = {
  a: 3,
  b: 4,
};
console.log(a, b); // 3 4

这里,我们将一个生成器函数赋值给了 Object.prototype[Symbol.iterator]

  • 生成器函数 (function*):生成器函数是一种特殊的函数,它可以通过 yield 关键字暂停执行并在后续恢复执行,并且可以多次返回(“产出”)值。生成器函数返回一个迭代器对象。
  • yield* 表达式yield* 后面可以跟一个可迭代对象(比如数组、字符串、另一个生成器),它会将可迭代对象的所有值依次“产出”。
  • Object.values(this)Object.values(this) 会返回当前对象 (this) 的所有属性值组成的一个新数组。

所以,这段代码的含义是:当我们对一个普通对象进行迭代时,实际上是在迭代该对象的所有属性值yield* Object.values(this) 就把这些属性值一个接一个地“产出”给迭代过程。

通过这样做,我们就成功地让普通对象变得可迭代了。现在,当我们对 { a: 3, b: 4 } 进行数组解构赋值 var [a, b] = { a: 3, b: 4 }; 时,JavaScript 引擎会查找该对象的 [Symbol.iterator] 属性,并调用它返回的迭代器。迭代器会依次返回 34,然后将它们分别赋值给 ab

📌 迭代器的基础回顾

为了更好地理解上面的代码,我们简单回顾一下迭代器的基础知识:

  • 可迭代对象 (Iterable):拥有 [Symbol.iterator] 属性的对象。
  • 迭代器对象 (Iterator):由可迭代对象的 [Symbol.iterator] 方法返回的对象。迭代器对象必须有一个 next() 方法。
  • next() 方法:调用迭代器的 next() 方法会返回一个包含两个属性的对象:
    • value:迭代的当前值。
    • done:一个布尔值,表示迭代是否完成。false 表示还有更多值,true 表示迭代已结束。

以数组为例:

const arr = [3, 4];
const iter = arr[Symbol.iterator](); // 获取数组的迭代器

console.log(iter.next()); // { value: 3, done: false }
console.log(iter.next()); // { value: 4, done: false }
console.log(iter.next()); // { value: undefined, done: true }

通过 next() 方法,我们可以一步步地遍历可迭代对象中的值。

⚠️ 注意事项

虽然通过修改 Object.prototype 可以实现一些有趣的功能,但在实际开发中需要非常谨慎。直接修改内置对象的原型可能会对其他代码或第三方库产生不可预测的影响,导致难以调试的问题。

这种修改原型的方式更适合于学习和理解迭代器的工作原理,或者在非常特殊的场景下(且能确保不会产生副作用)使用。

总结

通过为 Object.prototype 添加 [Symbol.iterator] 属性,我们让普通对象具备了迭代能力,从而可以使用数组的解构赋值语法来提取属性值。这展示了迭代器在 JavaScript 中的强大作用和灵活性。虽然这种直接修改原型的方式需要谨慎使用,但理解其背后的原理对于深入掌握 JavaScript 的迭代机制至关重要。