展开运算符执行单层浅拷贝,仅切断第一层引用;嵌套的引用类型仍共享内存,修改深层属性会导致原对象被污染。

展开运算符(...)在 JavaScript 中执行的是**单层浅拷贝**,它只切断第一层引用。一旦对象嵌套超过一层,内部的引用类型(如对象、数组)仍指向同一块堆内存——这时修改深层属性,原对象就会被意外改动。
为什么层级变深就失效
因为展开运算符本质是“逐个赋值第一层属性”:
- 遇到字符串、数字等原始类型:复制值,彼此独立
- 遇到对象、数组等引用类型:只复制地址,新旧变量仍共用同一内存块
比如 const copy = { ...original },相当于手动写了:copy.a = original.a(若 a 是对象,则只是把地址给了 copy.a)
再往下,copy.a.b.c 和 original.a.b.c 指向的还是同一个 c 所在的对象。
典型失效场景举例
看这段代码:
const user = {
name: 'Alice',
profile: {
settings: { theme: 'dark' },
tags: ['user', 'active']
}
};
const editUser = { ...user }; // 浅拷贝
editUser.profile.settings.theme = 'light'; // ✅ 改了!
editUser.profile.tags.push('vip'); // ✅ 改了!
console.log(user.profile.settings.theme); // 'light' ← 原对象被污染
console.log(user.profile.tags); // ['user', 'active', 'vip'] ← 同样被污染
原因很直接:editUser.profile 和 user.profile 是同一个对象;它们内部的 settings 和 tags 也都没被重新创建。
怎么验证是不是已经失效
遇到“改了副本,原数据也变了”,三步快速定位:
- 检查修改路径:如果改的是
obj.a.b.c这种三级或更深结构,基本可以确定是浅拷贝没覆盖到 - 用
===对比引用:console.log(editUser.profile === user.profile)→ 输出 true 就说明这一层根本没断开 - 回溯拷贝写法:确认是否只用了
{...obj}或[...arr],而没对嵌套部分做额外处理
想让深层也安全,得主动干预
不能指望展开运算符自动递归。需要按需补刀:
- 只改某一层?手动重写那一层:
{ ...user, profile: { ...user.profile, settings: { ...user.profile.settings } } } - 结构固定且不深?组合展开:
{ ...obj, nested: { ...obj.nested, deeper: { ...obj.nested.deeper } } } - 结构复杂或不确定深度?换方案:
structuredClone(obj)(现代浏览器)、或JSON.parse(JSON.stringify(obj))(仅限纯数据)
不复杂但容易忽略:展开运算符不是“万能复制键”,它只管开门,门后房间要不要重装,得你自己动手。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











