嘿,各位代码界的弄潮儿们,今天咱们来聊聊 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 }
瞧,就像变魔术一样,...obj1
把 obj1
的所有属性“展开”到新的对象里,然后 ...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) | 性能好,可以处理各种特殊情况 | 需要引入第三方库 | 需要高性能的深拷贝,或者需要处理各种特殊情况 |
第四幕:...
的高级用法,解锁更多姿势
...
除了可以合并对象和拷贝对象,还可以用于其他一些场景:
- 动态添加属性:
const key = 'age';
const obj = { name: 'Alice', ...{ [key]: 30 } }; //动态属性名
console.log(obj); // { name: 'Alice', age: 30 }
- 过滤属性:
虽然 ...
本身不能直接过滤属性,但可以结合解构赋值来实现类似的效果。
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 }
- 构建新的对象结构:
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"
// }
第五幕:注意事项,避开坑爹陷阱
null
和undefined
:
如果 ...
后面跟着 null
或 undefined
,不会报错,但也不会有任何效果。
const obj = { ...null, ...undefined, a: 1 };
console.log(obj); // { a: 1 }
- 属性覆盖:
记住,后面的对象属性会覆盖前面的同名属性。
- 性能:
虽然 ...
语法简洁,但对于大型对象,性能可能不如手动赋值。
总结陈词
...
运算符是 JavaScript 中一个非常强大的工具,可以简化对象操作,提高代码的可读性。但是,也要注意它的局限性,特别是浅拷贝的问题。在选择拷贝方式时,要根据实际情况进行权衡,选择最适合的方案。
好了,今天的讲座就到这里。希望大家能够熟练掌握 ...
运算符,在代码的世界里玩得更溜!下次再见!