一句话概括
浅拷贝「只复制第一层」:基本类型值独立、嵌套对象共享引用——{ ...obj } 改 name 没事,改 info.city 原对象也跟着变。
核心知识点
1. 展开运算符 — 最推荐的浅拷贝
1
2
3
4
5
6
7
8
| const user = { name: 'Alice', info: { city: 'Beijing' } };
const copy = { ...user };
copy.name = 'Bob'; // ✅ 第一层:互不影响
copy.info.city = 'Shanghai'; // ⚠️ 嵌套层:原对象也被改了!
console.log(user.name); // 'Alice'
console.log(user.info.city); // 'Shanghai' ← 被改了!
|
一句话记牢:... 只剥一层皮,里面还是同一块肉。
2. Object.assign — 会修改第一个参数
1
2
3
4
5
6
7
8
9
10
11
| const target = { a: 1 };
const result = Object.assign(target, { b: 2 });
console.log(target); // { a: 1, b: 2 } ← target 被改了!
console.log(result === target); // true ← 返回值就是 target
// ❌ 面试易错:这样写原对象被污染了
Object.assign(obj, { newProp: 'value' });
// ✅ 正确姿势:第一个参数给空对象
const safe = Object.assign({}, obj);
|
3. 数组浅拷贝四种写法,核心原理一致
1
2
3
4
5
6
7
8
9
10
11
| const arr = [1, 2, { x: 10 }];
// 四种等价写法:
const a = [...arr];
const b = arr.slice();
const c = arr.concat();
const d = Array.from(arr);
// 都一样:第一层独立,嵌套引用共享
a[2].x = 999;
console.log(arr[2].x); // 999 ← 全被改了
|
4. 不可枚举属性 & 原型链属性都不会被复制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| const parent = { shared: 'from prototype' };
const child = Object.create(parent);
child.own = 'own prop';
// 定义不可枚举属性
Object.defineProperty(child, 'secret', {
value: 'hidden',
enumerable: false,
});
const copy = { ...child };
console.log(copy); // { own: 'own prop' }
console.log(copy.shared); // undefined ← 原型链上的拿不到
console.log(copy.secret); // undefined ← 不可枚举的被跳过
|
只有可枚举的自有属性才会被浅拷贝——展开运算符和 Object.assign 都是这个规则。
5. Symbol 属性也会被复制(ES2018+)
1
2
3
4
5
| const key = Symbol('id');
const obj = { [key]: 'symbol value', name: 'test' };
const copy = { ...obj };
console.log(copy[key]); // 'symbol value' ← 别被面试官问倒
|
其实你每天都在用
React 状态更新
1
2
3
4
5
6
7
8
9
10
| const [user, setUser] = useState({ name: 'Alice', age: 20 });
// 更新单层属性 — 浅拷贝就够了
setUser({ ...user, age: 21 });
// 更新嵌套属性 — 需要手动展开每一层
setUser(prev => ({
...prev,
info: { ...prev.info, city: 'Shanghai' },
}));
|
React 靠引用比较判断要不要重新渲染,所以你每天都在 { ...prev }。
Redux / Zustand 里的不可变更新
1
2
3
4
5
6
7
8
9
10
11
12
13
| // 不改原数组,始终返回新数组
case 'ADD_TODO':
return { ...state, todos: [...state.todos, action.payload] };
case 'TOGGLE_TODO':
return {
...state,
todos: state.todos.map(todo =>
todo.id === action.id
? { ...todo, done: !todo.done }
: todo
),
};
|
函数默认配置合并
1
2
3
4
5
| function request(url, opts = {}) {
const defaults = { method: 'GET', timeout: 5000 };
const config = { ...defaults, ...opts };
return fetch(url, config);
}
|
函数参数处理 — rest 与 spread 对偶
1
2
3
4
5
| // rest:把剩余参数收起来
const [head, ...tail] = [1, 2, 3, 4]; // head=1, tail=[2,3,4]
// spread:把数组展开
const nums = [head, ...tail]; // [1, 2, 3, 4] ← 又拼回去了
|
数组增删改,不改原数组
1
2
3
4
5
6
7
8
9
10
| const list = [1, 2, 3, 4];
// 删除 index=1
const removed = [...list.slice(0, 1), ...list.slice(2)]; // [1, 3, 4]
// 插入:在 index=2 插入 99
const inserted = [...list.slice(0, 2), 99, ...list.slice(2)]; // [1, 2, 99, 3, 4]
// 更新 index=0 的值
const updated = list.map((v, i) => (i === 0 ? 100 : v)); // [100, 2, 3, 4]
|
常见误解
❌ 误区 1:「{ ...obj } 是深拷贝」
这是面试被挂最高频的错误。展开运算符只复制第一层,嵌套对象仍然是同一个引用。
1
2
3
4
| const obj = { a: { b: 1 } };
const copy = { ...obj };
copy.a.b = 2;
console.log(obj.a.b); // 2 ← 原对象变了!不是深拷贝
|
❌ 误区 2:「Object.assign({}, obj) 不会动原对象」
写法对了但理解错了:Object.assign 永远会修改第一个参数。你只是把空对象当 target 才避免了污染。
1
2
3
| const target = { a: 1 };
Object.assign(target, { a: 999 });
console.log(target.a); // 999 ← target 确实被改了
|
如果面试官问 Object.assign 会不会修改参数——答案是 会,会改第一个参数。
❌ 误区 3:「arr.slice() 会深拷贝数组元素」
slice() 对基本类型数组没问题,但元素是对象时依然是浅拷贝:
1
2
3
4
| const arr = [{ id: 1 }];
const copy = arr.slice();
copy[0].id = 999;
console.log(arr[0].id); // 999 ← 里面的对象还是同一个引用
|
❌ 误区 4:「浅拷贝会复制 getter / setter」
不会。展开运算符和 Object.assign 都会触发 getter 并把返回值作为普通值复制,拷贝后的对象里没有 getter 了。
1
2
3
4
5
6
7
8
9
10
| const obj = {
get count() {
console.log('getter 被触发了');
return 42;
},
};
const copy = { ...obj };
// 控制台输出: 'getter 被触发了'
console.log(copy.count); // 42 — 但不是通过 getter 拿的,就是个普通值
|
一句话总结
浅拷贝就是「换了个壳,没换里子」——记牢 { ...obj }、[...arr]、arr.slice() 三板斧,面试别踩「以为这是深拷贝」的坑就够了。