JS `Spread` 运算符 (`…`) 在对象中的应用:对象属性的合并与拷贝

嘿,各位代码界的弄潮儿们,今天咱们来聊聊 JavaScript 里那个神通广大的“小点点”—— Spread 运算符(...)。这玩意儿可不只是语法糖,它在对象操作中简直就是瑞士军刀,能合并属性、拷贝对象,玩得溜还能省不少事儿。

准备好了吗?咱们这就开讲!

第一幕:... 登场,对象合并的华丽舞台

想象一下,你有两个对象,各自藏着一些宝贝属性,现在你想把它们合二为一,变成一个超级对象。在 ... 没出现之前,你可能得用 Object.assign() 吭哧吭哧地手动搬运。

const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };

// 古老的办法
const mergedObj = Object.assign({}, obj1, obj2);
console.log(mergedObj); // { a: 1, b: 2, c: 3, d: 4 }

现在,有了 ...,一切都变得优雅起来:

const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };

// 优雅的办法
const mergedObj = { ...obj1, ...obj2 };
console.log(mergedObj); // { a: 1, b: 2, c: 3, d: 4 }

瞧,就像变魔术一样,...obj1obj1 的所有属性“展开”到新的对象里,然后 ...obj2 也做了同样的事情。最终,一个包含了所有属性的新对象就诞生了。

划重点: 后面的对象属性会覆盖前面的同名属性。这就像在舞台上,后来的演员会占据更显眼的位置。

const obj1 = { a: 1, b: 2 };
const obj2 = { a: 3, c: 4 };

const mergedObj = { ...obj1, ...obj2 };
console.log(mergedObj); // { a: 3, b: 2, c: 4 }  注意 a 的值被 obj2 覆盖了

第二幕:... 的拷贝大法,告别浅拷贝的陷阱

在 JavaScript 里,对象的赋值其实是指针的传递。这意味着,如果你直接把一个对象赋值给另一个对象,它们实际上指向的是同一块内存地址。修改其中一个,另一个也会跟着变,这就是所谓的“浅拷贝”。

const originalObj = { a: 1, b: { c: 2 } };
const copiedObj = originalObj; // 浅拷贝

copiedObj.a = 5;
console.log(originalObj.a); // 5  originalObj 也被修改了!

copiedObj.b.c = 10;
console.log(originalObj.b.c); // 10  深层属性也跟着变!

这可不是我们想要的!我们想要的是一个完全独立的对象,修改它不会影响到原来的对象。这时候,... 就派上用场了。它可以创建一个新的对象,并将原对象的所有属性复制到新对象中,实现“浅拷贝”。

const originalObj = { a: 1, b: { c: 2 } };
const copiedObj = { ...originalObj }; // 使用 ... 进行浅拷贝

copiedObj.a = 5;
console.log(originalObj.a); // 1  originalObj 的 a 属性没变

copiedObj.b.c = 10;
console.log(originalObj.b.c); // 10  糟糕!深层属性还是跟着变了!

等等!怎么 b.c 还是变了?这是因为 ... 只会拷贝对象的第一层属性,如果属性值是对象或数组,它只会拷贝引用,而不是创建一个新的对象或数组。这就是为什么它被称为“浅拷贝”。

第三幕:深拷贝的进阶之路,... 的局限与替代方案

既然 ... 只能实现浅拷贝,那怎么才能实现深拷贝呢?深拷贝是指创建一个完全独立的对象,包括所有嵌套的对象和数组,修改新对象不会影响到原来的对象。

方案一:JSON 序列化/反序列化

这是一种简单粗暴的方法,先把对象转换成 JSON 字符串,然后再把 JSON 字符串转换回对象。

const originalObj = { a: 1, b: { c: 2 } };
const copiedObj = JSON.parse(JSON.stringify(originalObj)); // 深拷贝

copiedObj.b.c = 10;
console.log(originalObj.b.c); // 2  originalObj 的 b.c 属性没变!

这种方法简单易懂,但也有一些缺点:

  • 性能问题: JSON 序列化/反序列化是一个比较耗时的操作,特别是对于大型对象。
  • 无法拷贝函数和 Symbol: JSON 只能表示基本数据类型和对象、数组,无法表示函数和 Symbol。
  • 循环引用问题: 如果对象中存在循环引用,JSON 序列化会报错。

方案二:递归拷贝

这是一种更灵活的方法,可以自定义拷贝规则,处理特殊类型的数据。

function deepClone(obj) {
  if (typeof obj !== 'object' || obj === null) {
    return obj; // 如果不是对象或 null,直接返回
  }

  const newObj = Array.isArray(obj) ? [] : {}; // 根据类型创建新对象

  for (let key in obj) {
    if (obj.hasOwnProperty(key)) { // 只拷贝自身属性
      newObj[key] = deepClone(obj[key]); // 递归拷贝
    }
  }

  return newObj;
}

const originalObj = { a: 1, b: { c: 2, d: () => console.log('hello') }, e: [1, 2, 3] };
const copiedObj = deepClone(originalObj);

copiedObj.b.c = 10;
console.log(originalObj.b.c); // 2
copiedObj.b.d(); // 报错,函数无法被拷贝

递归拷贝可以处理循环引用,拷贝函数等,但它需要更多的代码,并且可能存在性能问题。

方案三:使用第三方库

有很多第三方库提供了深拷贝的功能,例如 Lodash 的 _.cloneDeep() 方法。这些库通常经过了优化,性能更好,并且可以处理各种特殊情况。

// 需要先安装 lodash:npm install lodash
const _ = require('lodash');

const originalObj = { a: 1, b: { c: 2 } };
const copiedObj = _.cloneDeep(originalObj);

copiedObj.b.c = 10;
console.log(originalObj.b.c); // 2

表格总结:拷贝方式大比拼

拷贝方式 优点 缺点 适用场景
浅拷贝(... 简单易用,性能好 只能拷贝第一层属性,深层属性仍然共享引用 只需要拷贝第一层属性,或者确定对象中没有嵌套对象/数组
JSON 序列化/反序列化 简单易懂 性能较差,无法拷贝函数和 Symbol,存在循环引用问题 对象结构简单,不需要拷贝函数和 Symbol,不存在循环引用
递归拷贝 灵活,可以自定义拷贝规则,处理特殊类型的数据 代码量大,可能存在性能问题 需要处理特殊类型的数据,或者需要自定义拷贝规则
第三方库(如 Lodash) 性能好,可以处理各种特殊情况 需要引入第三方库 需要高性能的深拷贝,或者需要处理各种特殊情况

第四幕:... 的高级用法,解锁更多姿势

... 除了可以合并对象和拷贝对象,还可以用于其他一些场景:

  1. 动态添加属性:
const key = 'age';
const obj = { name: 'Alice', ...{ [key]: 30 } }; //动态属性名
console.log(obj); // { name: 'Alice', age: 30 }
  1. 过滤属性:

虽然 ... 本身不能直接过滤属性,但可以结合解构赋值来实现类似的效果。

const obj = { a: 1, b: 2, c: 3 };
const { a, ...rest } = obj; // 解构赋值,将 a 属性提取出来,剩下的属性放到 rest 对象中

console.log(a);    // 1
console.log(rest); // { b: 2, c: 3 }
  1. 构建新的对象结构:
const user = {
  id: 123,
  name: "John Doe",
  address: {
    street: "123 Main St",
    city: "Anytown"
  }
};

const formattedUser = {
  userId: user.id,
  userName: user.name,
  ...user.address // 将 address 对象的属性展开到新的对象中
};

console.log(formattedUser);
// 输出:
// {
//   userId: 123,
//   userName: "John Doe",
//   street: "123 Main St",
//   city: "Anytown"
// }

第五幕:注意事项,避开坑爹陷阱

  1. nullundefined

如果 ... 后面跟着 nullundefined,不会报错,但也不会有任何效果。

const obj = { ...null, ...undefined, a: 1 };
console.log(obj); // { a: 1 }
  1. 属性覆盖:

记住,后面的对象属性会覆盖前面的同名属性。

  1. 性能:

虽然 ... 语法简洁,但对于大型对象,性能可能不如手动赋值。

总结陈词

... 运算符是 JavaScript 中一个非常强大的工具,可以简化对象操作,提高代码的可读性。但是,也要注意它的局限性,特别是浅拷贝的问题。在选择拷贝方式时,要根据实际情况进行权衡,选择最适合的方案。

好了,今天的讲座就到这里。希望大家能够熟练掌握 ... 运算符,在代码的世界里玩得更溜!下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注