引言:为什么深浅拷贝是前端开发的核心知识?
在 JavaScript 的日常开发中,对象和数组的拷贝操作无处不在。无论是配置合并、状态管理、数据缓存,还是组件通信,我们几乎每天都在与“拷贝”打交道。然而,看似简单的 = 赋值、Object.assign() 或 JSON.parse(JSON.stringify()),背后却隐藏着 JavaScript 引用机制的深刻原理。
更关键的是,在前端面试中,深拷贝与浅拷贝是必考内容。它不仅考察你对语言特性的理解,还测试你对内存管理、递归、类型判断、循环引用等核心编程能力的掌握程度。
本文将从 Object.assign() 出发,系统性地讲解深浅拷贝的底层原理、API 使用细节、常见陷阱、高级实现方案,并最终模拟一场真实的面试问答,带你从“会用”走向“精通”。
一、从 Object.assign() 开始:浅拷贝的起点
1.1 什么是 Object.assign()?
Object.assign() 是 ES6 引入的一个方法,用于将一个或多个源对象(source objects)的所有可枚举(enumerable)属性复制到目标对象(target object),并返回修改后的目标对象。
语法:
Object.assign(target, ...sources)
target:目标对象(会被修改)...sources:一个或多个源对象- 返回值:被修改后的目标对象
示例:
const target = { a: 1 };
const source = { b: 2 };
const result = Object.assign(target, source);
console.log(result); // { a: 1, b: 2 }
console.log(target === result); // true
✅ 关键点:
Object.assign()返回的是目标对象本身,而不是一个新对象!
这意味着:target 和 result 指向同一个内存地址。Object.assign() 是一个浅拷贝(Shallow Copy),它只复制对象的第一层属性。
1.2 浅拷贝的本质:值 vs 引用
要理解深浅拷贝,必须先理解 JavaScript 的数据类型与内存分配机制。
1.2.1 基本数据类型(值类型)
- 包括:
number、string、boolean、null、undefined、symbol、bigint - 存储在**栈内存(stack)**中
- 赋值时是值传递,互不影响
let a = 1;
let b = a;
b = 2;
console.log(a); // 1
1.2.2 复杂数据类型(引用类型)
- 包括:
object、array、function - 存储在**堆内存(heap)**中
- 变量存储的是指向堆内存的地址(引用)
- 赋值时是引用传递,多个变量可能指向同一个对象
let obj1 = { name: 'zhangsan', age: 18 };
let obj2 = obj1; // obj2 指向 obj1 的内存地址
obj2.age = 20;
console.log(obj1.age); // 20 —— obj1 也被修改了!
🔥 这就是“引用式赋值”的陷阱:没有真正“拷贝”,只是共享了同一个对象。
1.3 Object.assign() 的浅拷贝行为
我们通过一个经典例子来验证 Object.assign() 的浅拷贝特性:
const target = { a: 1 };
const source = {
b: {
name: 'zhangsan',
age: 18,
hobby: ['吃饭', '睡觉', '打豆豆']
},
c: 1
};
Object.assign(target, source);
// 修改嵌套对象
target.b.age = 20;
target.b.hobby.push('学习');
target.c = 2;
console.log(source.b.age); // 20
console.log(source.b.hobby); // ['吃饭', '睡觉', '打豆豆', '学习']
console.log(source.c); // 1
📌 结论:
source.b是一个对象,Object.assign()只复制了它的引用,所以target.b和source.b指向同一个对象。- 修改
target.b.age会影响source.b.age。- 而
c是基本类型,复制的是值,互不影响。
1.4 Object.assign() 的使用场景
场景 1:配置对象合并(最常见)
function createUser(options) {
const defaults = {
name: 'zhangsan',
age: 18,
isAdmin: false
};
// 合并默认配置和用户传参
const config = Object.assign({}, defaults, options);
console.log(config);
}
createUser({ name: 'lisi', age: 20 });
// 输出: { name: 'lisi', age: 20, isAdmin: false }
✅ 技巧:目标对象设为
{}空对象,避免污染原对象。
场景 2:环境配置优先级
const baseConfig = { api: '/api', timeout: 500 };
const envConfig = { timeout: 1000, debug: true };
const finalConfig = Object.assign({}, baseConfig, envConfig);
console.log(finalConfig);
// 输出: { api: '/api', timeout: 1000, debug: true }
✅ 后面的源对象会覆盖前面的同名属性。
1.5 Object.assign() 的边界情况
1.5.1 传入 null 或 undefined
const target = { a: 1 };
Object.assign(target, null);
Object.assign(target, undefined);
console.log(target); // { a: 1 } —— 不会报错,但也不会拷贝任何属性
1.5.2 单个参数
const obj = { name: '张三' };
Object.assign(obj); // 相当于 Object.assign(obj, {})
console.log(obj); // { name: '张三' } —— 无变化
1.5.3 Symbol 作为键
const s = Symbol('id');
const source = { [s]: 123, a: 1 };
const target = [];
Object.assign(target, source);
console.log(target); // [1] —— Symbol 键的属性被忽略!
⚠️ 注意:
Object.assign()会拷贝 Symbol 键的属性,但目标如果是数组,Symbol 键不会被设置为数组索引。
二、深拷贝的常用方法:JSON.parse(JSON.stringify())
虽然 Object.assign() 是浅拷贝,但有一个“取巧”的方法可以实现深拷贝:
const newObj = JSON.parse(JSON.stringify(source));
2.1 原理:序列化 + 反序列化
JSON.stringify():将对象序列化为 JSON 字符串JSON.parse():将 JSON 字符串反序列化为新对象- 由于序列化过程会“扁平化”对象结构,新对象与原对象完全独立
示例:
const source = {
b: {
name: 'zhangsan',
age: 18,
hobby: ['吃饭', '睡觉', '打豆豆']
},
c: 1
};
const newObj = JSON.parse(JSON.stringify(source));
newObj.b.age = 20;
newObj.b.hobby.push('学习');
newObj.c = 2;
console.log(source.b.age); // 18
console.log(source.b.hobby); // ['吃饭', '睡觉', '打豆豆']
console.log(source.c); // 1
✅ 成功实现深拷贝!
2.2 JSON.parse(JSON.stringify()) 的致命缺陷
尽管简单有效,但这种方法有严重限制,不能用于所有场景。
缺陷 1:无法处理函数
const obj = { fn: function() { console.log('hello'); } };
const copy = JSON.parse(JSON.stringify(obj));
console.log(copy.fn); // undefined
❌ 函数不是合法的 JSON 值,会被忽略。
缺陷 2:无法处理 Symbol
const s = Symbol('id');
const obj = { [s]: 'symbol value' };
const copy = JSON.parse(JSON.stringify(obj));
console.log(copy[s]); // undefined
❌
Symbol不是合法的 JSON 值。
缺陷 3:undefined 会被忽略
const obj = { a: undefined, b: 1 };
const copy = JSON.parse(JSON.stringify(obj));
console.log(copy); // { b: 1 } —— a 属性消失
❌
undefined不是合法的 JSON 值。
缺陷 4:循环引用会报错
const obj = { name: 'obj' };
obj.self = obj; // 循环引用
JSON.stringify(obj); // TypeError: Converting circular structure to JSON
❌
JSON.stringify()无法序列化循环引用的对象。
缺陷 5:Date 对象会变成字符串
const obj = { date: new Date() };
const copy = JSON.parse(JSON.stringify(obj));
console.log(copy.date); // "2025-08-14T15:30:00.000Z" —— 字符串,不是 Date 对象
⚠️ 类型丢失!
缺陷 6:RegExp、Error、Map、Set 等特殊对象无法正确拷贝
const obj = { reg: /abc/, map: new Map() };
const copy = JSON.parse(JSON.stringify(obj));
console.log(copy.reg); // {} —— 正则表达式变空对象
console.log(copy.map); // {} —— Map 变空对象
❌ 特殊对象被错误地序列化。
2.3 总结:JSON.parse(JSON.stringify()) 的适用场景
| 场景 | 是否适用 |
|---|---|
| 纯数据对象(只有基本类型和嵌套对象/数组) | ✅ 推荐 |
| 包含函数、Symbol、undefined | ❌ 不适用 |
| 包含 Date、RegExp、Map、Set | ⚠️ 类型丢失,慎用 |
| 循环引用对象 | ❌ 会报错 |
✅ 结论:仅适用于简单的、纯数据的 JSON 结构。
三、手写深拷贝:从基础到高级
当 JSON.parse(JSON.stringify()) 无法满足需求时,我们必须手写深拷贝函数。
3.1 基础版本:递归拷贝
function clone(source) {
if (typeof source !== 'object' || source === null) {
return source; // 基本类型或 null 直接返回
}
const cloneTarget = Array.isArray(source) ? [] : {};
for (let key in source) {
if (source.hasOwnProperty(key)) {
cloneTarget[key] = clone(source[key]); // 递归拷贝
}
}
return cloneTarget;
}
测试:
const target = {
field1: 1,
field2: undefined,
field3: 'hxt',
field4: {
child: 'child',
child2: { child2: 'child2' }
},
field5: [2, 4, 8]
};
const obj = clone(target);
obj.field4.child = 'child2';
console.log(target.field4.child); // 'child' —— 未受影响,深拷贝成功
✅ 基础功能实现!
3.2 高级版本:解决循环引用(WeakMap 缓存)
如果对象存在循环引用,上述递归版本会栈溢出(Stack Overflow)。
问题示例:
const target = { a: 1 };
target.self = target; // 循环引用
clone(target); // Maximum call stack size exceeded
解决方案:使用 WeakMap 缓存已拷贝的对象
function deepClone(target, map = new WeakMap()) {
// 处理基本类型和 null
if (typeof target !== 'object' || target === null) {
return target;
}
// 处理循环引用
if (map.has(target)) {
return map.get(target);
}
// 初始化新对象
const cloneTarget = Array.isArray(target) ? [] : {};
// 缓存当前对象,避免重复拷贝
map.set(target, cloneTarget);
// 遍历所有可枚举属性(包括 Symbol 键)
const keys = [...Object.keys(target), ...Object.getOwnPropertySymbols(target)];
for (let key of keys) {
const descriptor = Object.getOwnPropertyDescriptor(target, key);
if (descriptor.enumerable || key === Symbol.for('non-enumerable')) {
cloneTarget[key] = deepClone(target[key], map);
}
}
return cloneTarget;
}
测试循环引用:
const obj = { name: 'obj' };
obj.self = obj;
const copy = deepClone(obj);
console.log(copy.self === copy); // true —— 循环引用被正确处理
✅ 成功解决循环引用问题!
3.3 完整版本:支持所有内置对象
真正的深拷贝应支持 Date、RegExp、Map、Set、Function 等。
function deepClone(target, map = new WeakMap()) {
// 1. 基本类型和 null
if (typeof target !== 'object' || target === null) {
return target;
}
// 2. 处理循环引用
if (map.has(target)) {
return map.get(target);
}
// 3. 处理特殊对象
const constructors = {
'[object Date]': (t) => new Date(t),
'[object RegExp]': (t) => new RegExp(t.source, t.flags),
'[object Map]': (t) => {
const mapCopy = new Map();
for (let [key, value] of t) {
mapCopy.set(deepClone(key, map), deepClone(value, map));
}
return mapCopy;
},
'[object Set]': (t) => {
const setCopy = new Set();
for (let value of t) {
setCopy.add(deepClone(value, map));
}
return setCopy;
},
'[object Promise]': (t) => {
// Promise 通常不拷贝,返回原对象或新建
return t;
},
'[object Function]': (t) => {
// 函数:返回原函数(不可拷贝)或克隆函数体(复杂)
return t;
}
};
const tag = Object.prototype.toString.call(target);
if (constructors[tag]) {
const copy = constructors[tag](target);
map.set(target, copy);
return copy;
}
// 4. 普通对象和数组
const cloneTarget = Array.isArray(target) ? [] : {};
map.set(target, cloneTarget);
const keys = [...Object.keys(target), ...Object.getOwnPropertySymbols(target)];
for (let key of keys) {
const descriptor = Object.getOwnPropertyDescriptor(target, key);
if (descriptor.enumerable) {
cloneTarget[key] = deepClone(target[key], map);
}
}
return cloneTarget;
}
功能总结:
- ✅ 支持基本类型
- ✅ 支持对象、数组
- ✅ 支持
Date、RegExp、Map、Set - ✅ 支持
Symbol键 - ✅ 支持循环引用(
WeakMap) - ✅ 支持函数(返回原函数)
- ✅ 处理
null
四、其他浅拷贝方法
除了 Object.assign(),还有以下常用浅拷贝方法:
| 方法 | 说明 |
|---|---|
Array.prototype.slice() | 返回数组浅拷贝 |
Array.prototype.concat() | 合并数组,常用于拷贝 |
扩展运算符 ... | ES6 语法,简洁易读 |
示例:
const arr = [1, 2, { name: 'zhangsan' }];
const arr1 = arr.slice(); // 浅拷贝
const arr2 = arr.concat(); // 浅拷贝
const arr3 = [...arr]; // 浅拷贝
arr1[2].name = 'lisi';
console.log(arr[2].name); // 'lisi' —— 引用共享
✅ 都只拷贝第一层。
五、Map 与 WeakMap:ES6 的新数据结构
5.1 Map:键可以是任意类型
const target = new Map();
const obj = { a: 1 };
target.set(obj, 'value');
console.log(target.get(obj)); // 'value'
obj = null; // 手动释放引用
// 但 Map 仍持有 obj 的引用,不会被垃圾回收
5.2 WeakMap:弱引用,可被垃圾回收
const target2 = new WeakMap();
const obj2 = { name: 'zhangsan' };
target2.set(obj2, 'secret');
obj2 = null; // 对象可被垃圾回收
console.log(target2.get(obj2)); // undefined
🔥
WeakMap常用于缓存、私有属性,避免内存泄漏。
六、面试实战
请解释 JavaScript 中的深拷贝和浅拷贝,并手写一个完整的深拷贝函数。
1. 基本概念
- 浅拷贝(Shallow Copy):只复制对象的第一层属性。如果属性是基本类型,复制值;如果是引用类型,复制的是引用地址。因此,修改嵌套对象会影响原对象。
- 深拷贝(Deep Copy):递归复制对象的所有层级,新对象与原对象完全独立,互不影响。
2. 浅拷贝方法
Object.assign(target, source):将源对象的可枚举属性复制到目标对象。注意:返回的是目标对象本身,不是新对象。- 数组:
slice()、concat()、扩展运算符... - 特点:只复制第一层,嵌套对象共享引用。
3. 深拷贝方法
-
JSON.parse(JSON.stringify(obj)):简单但有严重限制:- 无法处理函数、
Symbol、undefined Date变字符串- 循环引用会报错
- 不支持
Map、Set等 - 仅适用于纯数据对象。
- 无法处理函数、
-
手写深拷贝:更灵活,可处理各种边界情况。
4. 手写深拷贝实现
function deepClone(target, map = new WeakMap()) {
// 1. 基本类型和 null
if (typeof target !== 'object' || target === null) {
return target;
}
// 2. 循环引用检测
if (map.has(target)) {
return map.get(target);
}
// 3. 处理内置对象
const tag = Object.prototype.toString.call(target);
const constructors = {
'[object Date]': () => new Date(target),
'[object RegExp]': () => new RegExp(target.source, target.flags),
'[object Map]': () => {
const mapCopy = new Map();
for (let [k, v] of target) {
mapCopy.set(deepClone(k, map), deepClone(v, map));
}
return mapCopy;
},
'[object Set]': () => {
const setCopy = new Set();
for (let v of target) {
setCopy.add(deepClone(v, map));
}
return setCopy;
},
'[object Function]': () => target // 函数通常不拷贝
};
if (constructors[tag]) {
const copy = constructors[tag]();
map.set(target, copy);
return copy;
}
// 4. 普通对象/数组
const cloneTarget = Array.isArray(target) ? [] : {};
map.set(target, cloneTarget);
// 5. 拷贝所有属性(包括 Symbol)
const keys = [...Object.keys(target), ...Object.getOwnPropertySymbols(target)];
for (let key of keys) {
if (Object.getOwnPropertyDescriptor(target, key).enumerable) {
cloneTarget[key] = deepClone(target[key], map);
}
}
return cloneTarget;
}
5. 关键点总结
- 使用
WeakMap避免循环引用导致的栈溢出。 - 正确处理
Date、RegExp、Map、Set等特殊对象。 - 支持
Symbol作为键。 - 保持属性的可枚举性。
- 函数通常不拷贝,直接返回原函数。
6. 应用场景
- 状态管理(如 Redux 中的不可变更新)
- 配置备份
- 数据缓存
- 表单重置
结语
深浅拷贝不仅是 JavaScript 的基础概念,更是理解语言内存模型、引用机制的关键。掌握 Object.assign()、JSON.parse(JSON.stringify()) 的局限性,并能手写一个健壮的深拷贝函数,是每个前端开发者必备的技能。
在实际开发中,建议:
- 简单场景用
JSON.stringify - 复杂场景用 Lodash 的
_.cloneDeep - 面试时手写
deepClone展示基本功
希望本文能帮助你彻底掌握深浅拷贝,从容应对面试与实战!