文章

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

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

深入对比 JavaScript 中各种浅拷贝方法的实现原理、使用场景和性能差异,掌握正确的拷贝姿势。

一、背景与问题

在 JavaScript 中,对象和数组是引用类型,直接赋值只是复制引用:

1
2
3
4
5
const obj = { name: 'Alice', age: 20 };
const copy = obj;  // 只是复制引用

copy.age = 30;
console.log(obj.age);  // 30 - 原对象也被修改!

浅拷贝:创建新对象,复制第一层属性,但嵌套对象仍是引用。

1
2
3
4
5
6
7
8
const obj = { name: 'Alice', info: { city: 'Beijing' } };
const shallowCopy = { ...obj };

shallowCopy.name = 'Bob';        // 修改第一层
shallowCopy.info.city = 'Shanghai';  // 修改嵌套对象

console.log(obj.name);           // 'Alice' - 未受影响
console.log(obj.info.city);      // 'Shanghai' - 被修改了!

二、核心概念与定义

2.1 浅拷贝的定义

创建一个新对象/数组,将原对象/数组的第一层属性/元素复制过来:

  • 基本类型(string、number、boolean、null、undefined、symbol、bigint):值复制
  • 引用类型(object、array、function):引用复制
1
2
3
4
5
6
7
8
9
10
11
原对象                    浅拷贝后
┌─────────────┐         ┌─────────────┐
│ name: 'Alice'│ ──────→ │ name: 'Alice'│ (值复制)
│ age: 20     │ ──────→ │ age: 20     │ (值复制)
│ info: ──────┼──┐      │ info: ──────┼──┐
└─────────────┘  │      └─────────────┘  │
                 │                      │
                 ▼                      ▼
            ┌──────────┐           ┌──────────┐
            │city:'BJ' │ ← 同一引用 → │city:'BJ' │
            └──────────┘           └──────────┘

2.2 浅拷贝 vs 深拷贝

特性浅拷贝深拷贝
第一层值复制值复制
嵌套对象引用复制递归值复制
性能
实现复杂度简单复杂
适用场景单层对象多层嵌套对象

三、最小示例

3.1 对象浅拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const obj = { a: 1, b: 2, c: { d: 3 } };

// 方式一:Object.assign
const copy1 = Object.assign({}, obj);

// 方式二:展开运算符
const copy2 = { ...obj };

// 方式三:手动复制
const copy3 = {};
for (const key in obj) {
  if (obj.hasOwnProperty(key)) {
    copy3[key] = obj[key];
  }
}

3.2 数组浅拷贝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const arr = [1, 2, { a: 3 }];

// 方式一:slice
const copy1 = arr.slice();

// 方式二:concat
const copy2 = arr.concat();

// 方式三:展开运算符
const copy3 = [...arr];

// 方式四:Array.from
const copy4 = Array.from(arr);

// 方式五:copyWithin(需先复制长度)
const copy5 = [...arr];

四、核心知识点拆解

4.1 Object.assign

语法Object.assign(target, ...sources)

1
2
3
4
5
6
7
8
9
10
11
// 合并多个源对象到目标对象
const target = { a: 1 };
const source1 = { b: 2 };
const source2 = { c: 3 };

const result = Object.assign(target, source1, source2);
console.log(result);  // { a: 1, b: 2, c: 3 }
console.log(target);  // { a: 1, b: 2, c: 3 } - target 被修改!

// 浅拷贝:传入空对象作为 target
const copy = Object.assign({}, original);

特点

  • 会修改 target 对象
  • 返回值是 target 对象
  • 后面的源对象会覆盖前面的同名属性
  • 会复制可枚举的自有属性

注意

1
2
3
4
5
6
7
8
9
10
11
// 不会复制 Symbol 属性?会!
const obj = { a: 1, [Symbol('b')]: 2 };
const copy = Object.assign({}, obj);
console.log(Object.getOwnPropertySymbols(copy));  // [Symbol(b)]

// 不会复制继承属性
const parent = { a: 1 };
const child = Object.create(parent);
child.b = 2;
const copy = Object.assign({}, child);
console.log(copy);  // { b: 2 } - 没有 a

4.2 展开运算符(Spread Operator)

对象展开{ ...obj }

1
2
3
4
5
6
7
8
9
10
11
const obj = { a: 1, b: 2, c: 3 };
const copy = { ...obj };

// 合并多个对象(后者覆盖前者)
const merged = { ...obj1, ...obj2, ...obj3 };

// 添加新属性
const extended = { ...obj, d: 4 };

// 覆盖属性
const modified = { ...obj, a: 100 };

数组展开[...arr]

1
2
3
4
5
6
7
8
const arr = [1, 2, 3];
const copy = [...arr];

// 合并数组
const merged = [...arr1, ...arr2];

// 添加元素
const extended = [...arr, 4, 5];

Object.assign vs 展开运算符

特性Object.assign展开运算符
语法Object.assign({}, obj){ ...obj }
可读性较差更好
性能略慢略快
修改 target不会
Getter 处理执行 getter执行 getter

4.3 数组方法

slice()

1
2
3
4
5
6
7
const arr = [1, 2, 3, 4, 5];

// 浅拷贝整个数组
const copy = arr.slice();

// 提取部分元素
const part = arr.slice(1, 3);  // [2, 3]

concat()

1
2
3
4
5
6
7
const arr = [1, 2, 3];

// 浅拷贝
const copy = arr.concat();

// 合并数组
const merged = arr.concat([4, 5]);

Array.from()

1
2
3
4
5
6
7
8
9
10
11
const arr = [1, 2, 3];

// 浅拷贝
const copy = Array.from(arr);

// 类数组转数组
const arrayLike = { 0: 'a', 1: 'b', length: 2 };
const arr = Array.from(arrayLike);  // ['a', 'b']

// 带 map 函数
const doubled = Array.from(arr, x => x * 2);

4.4 性能对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 性能测试
const obj = { a: 1, b: 2, c: 3, d: 4, e: 5 };
const N = 1000000;

console.time('Object.assign');
for (let i = 0; i < N; i++) {
  Object.assign({}, obj);
}
console.timeEnd('Object.assign');

console.time('Spread');
for (let i = 0; i < N; i++) {
  const copy = { ...obj };
}
console.timeEnd('Spread');

// 结果(Chrome):
// Object.assign: ~150ms
// Spread: ~100ms

结论:展开运算符性能略优于 Object.assign。


五、实战案例

5.1 React 状态更新

1
2
3
4
5
6
7
8
9
10
11
// React 中使用浅拷贝更新状态
const [user, setUser] = useState({ name: 'Alice', age: 20 });

// 更新单个属性
setUser({ ...user, age: 21 });

// 更新嵌套属性(需要深拷贝或手动处理)
setUser({
  ...user,
  info: { ...user.info, city: 'Shanghai' }
});

5.2 Redux Reducer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Redux reducer 使用浅拷贝保持不可变性
function reducer(state, action) {
  switch (action.type) {
    case 'UPDATE_USER':
      return {
        ...state,
        user: {
          ...state.user,
          name: action.payload
        }
      };
    case 'ADD_ITEM':
      return {
        ...state,
        items: [...state.items, action.payload]
      };
    default:
      return state;
  }
}

5.3 数组操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 添加元素
const newArr = [...arr, newItem];

// 删除元素
const newArr = arr.filter(item => item.id !== id);
// 或
const newArr = [...arr.slice(0, index), ...arr.slice(index + 1)];

// 更新元素
const newArr = arr.map(item => 
  item.id === id ? { ...item, updated: true } : item
);

// 插入元素
const newArr = [...arr.slice(0, index), newItem, ...arr.slice(index)];

5.4 函数参数默认值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 合并配置对象
function createConfig(userConfig = {}) {
  const defaultConfig = {
    timeout: 5000,
    retries: 3,
    headers: {}
  };
  
  return {
    ...defaultConfig,
    ...userConfig,
    headers: {
      ...defaultConfig.headers,
      ...userConfig.headers
    }
  };
}

六、底层原理

6.1 属性描述符

Object.assign 和展开运算符都会复制可枚举的自有属性

1
2
3
4
5
6
7
8
9
10
11
12
13
const obj = {};

// 定义不可枚举属性
Object.defineProperty(obj, 'hidden', {
  value: 'secret',
  enumerable: false
});

// 定义可枚举属性
obj.visible = 'public';

const copy = { ...obj };
console.log(copy);  // { visible: 'public' } - hidden 未被复制

6.2 Getter/Setter 处理

1
2
3
4
5
6
7
8
9
10
const obj = {
  get value() {
    console.log('getter called');
    return 42;
  }
};

const copy = { ...obj };
// getter 被执行,copy.value 是 42,不再是 getter
console.log(copy.value);  // 42(无 getter 日志)

6.3 原型链

1
2
3
4
5
6
7
8
// 展开运算符不会复制原型链上的属性
const parent = { a: 1 };
const child = Object.create(parent);
child.b = 2;

const copy = { ...child };
console.log(copy);  // { b: 2 } - a 在原型链上,未复制
console.log(copy.a);  // undefined

七、高频面试题解析

Q1:Object.assign 和展开运算符的区别?

答案

区别点Object.assign展开运算符
语法函数调用语法糖
返回值修改后的 target新对象
性能略慢略快
可读性较差更好
1
2
3
4
5
6
7
8
9
// Object.assign 会修改 target
const target = { a: 1 };
const result = Object.assign(target, { b: 2 });
console.log(target === result);  // true

// 展开运算符不会修改原对象
const obj = { a: 1 };
const copy = { ...obj, b: 2 };
console.log(obj);  // { a: 1 }

Q2:如何实现一个浅拷贝函数?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function shallowCopy(source) {
  if (source === null || typeof source !== 'object') {
    return source;
  }
  
  // 数组
  if (Array.isArray(source)) {
    return source.slice();
  }
  
  // 对象
  const copy = {};
  for (const key in source) {
    if (source.hasOwnProperty(key)) {
      copy[key] = source[key];
    }
  }
  return copy;
}

Q3:浅拷贝会复制 Symbol 属性吗?

答案:Object.assign 会,展开运算符也会(ES2018+)。

1
2
3
4
5
6
7
8
const sym = Symbol('key');
const obj = { [sym]: 'value' };

const copy1 = Object.assign({}, obj);
console.log(copy1[sym]);  // 'value'

const copy2 = { ...obj };
console.log(copy2[sym]);  // 'value'

Q4:为什么 React 推荐使用浅拷贝?

答案

  1. 不可变性:浅拷贝创建新对象,不修改原对象
  2. 性能:浅拷贝比深拷贝快
  3. 引用比较:React 通过引用比较判断状态变化
  4. 浅比较:React.memo、useMemo 使用浅比较

八、总结与扩展

核心要点

  1. Object.assign:会修改 target,适合合并多个对象
  2. 展开运算符:语法简洁,性能更好,推荐使用
  3. 数组方法:slice、concat、Array.from 都可实现浅拷贝
  4. 嵌套对象:浅拷贝后仍是引用,修改会影响原对象

方法选择指南

场景推荐方法
对象浅拷贝{ ...obj }
数组浅拷贝[...arr]arr.slice()
合并对象{ ...obj1, ...obj2 }
合并数组[...arr1, ...arr2]
配置合并Object.assign({}, defaults, options)

扩展阅读

  • structuredClone:浏览器原生深拷贝 API
  • Immer:不可变数据操作库
  • Immutable.js:持久化数据结构
  • Lodash.clone:支持自定义浅拷贝

浅拷贝是前端开发的基础操作,理解其原理和限制,才能正确处理复杂数据结构。

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