文章

浅拷贝方法对比分析深度解析

浅拷贝方法对比分析深度解析

一句话概括

浅拷贝「只复制第一层」:基本类型值独立、嵌套对象共享引用——{ ...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() 三板斧,面试别踩「以为这是深拷贝」的坑就够了。

本文由作者按照 CC BY 4.0 进行授权

© 独行的风. 保留部分权利。

本站采用 Jekyll 主题 Chirpy

本站总访问量 本站访客数 本文阅读量