前言:一个关于复印机的故事
想象一下,你有一份重要的文件需要复印。如果你使用的是普通复印机(浅拷贝),当你修改复印件上的内容时,原文件不会受到影响;但如果你修改的是复印件上粘着的便利贴(引用对象),那么原文件上的便利贴也会跟着改变。而深拷贝就像是一台神奇的复印机,它能完整复制文件以及上面所有的便利贴,并且修改复印件不会影响原件。
这就是编程中浅拷贝与深拷贝的核心区别。作为前端开发者,理解这个概念至关重要,因为它关系到我们如何处理数据,避免意外的副作用。
一、基础篇:认识拷贝
1.1 赋值 vs 拷贝
在JavaScript中,变量赋值有两种基本方式:
// 简单数据类型的赋值(复印)
let a = 100;
let b = a; // 真正的拷贝
b = 200;
console.log(a); // 100 - 不受影响
// 复杂数据类型的赋值(贴标签)
let obj1 = { name: '张三' };
let obj2 = obj1; // 只是引用,不是拷贝
obj2.name = '李四';
console.log(obj1.name); // '李四' - 原对象被修改了!
内存模型解释:
- 简单数据类型(Number, String, Boolean等)直接存储在栈内存中,拷贝时创建新值
- 复杂数据类型(Object, Array等)在堆内存中存储,变量只是保存指向堆内存的指针
1.2 浅拷贝的实现方式
1.2.1 Object.assign()
Object.assign() 是ES6引入的浅拷贝方法,它的行为特点:
const target = { a: 1 };
const source = { b: { name: '小明' } };
const result = Object.assign(target, source);
// 修改第一层属性
result.a = 2;
console.log(target.a); // 2 - 目标对象被修改
// 修改嵌套属性
result.b.name = '小红';
console.log(source.b.name); // '小红' - 源对象也被修改了!
关键点:
- 只拷贝对象自身的可枚举属性
- 是修改目标对象而非创建新对象
- 对于嵌套对象,只拷贝引用(浅拷贝)
1.2.2 数组的浅拷贝方法
// 方法1:slice()
const arr1 = [1, 2, { name: '张三' }];
const arr2 = arr1.slice();
arr2[2].name = '李四';
console.log(arr1[2].name); // '李四' - 原数组被修改
// 方法2:concat()
const arr3 = [].concat(arr1);
// 方法3:展开运算符
const arr4 = [...arr1];
1.3 浅拷贝的典型应用场景
1.3.1 合并配置对象
function initApp(options) {
const defaults = {
theme: 'light',
fontSize: 14,
apiBase: '/api'
};
// 用户配置覆盖默认配置
const config = Object.assign({}, defaults, options);
console.log(config);
}
initApp({ theme: 'dark' });
1.3.2 创建对象副本避免污染原对象
const original = { x: 1, y: 2 };
const copy = Object.assign({}, original);
copy.x = 3; // 不影响original
二、进阶篇:深入深拷贝
2.1 为什么需要深拷贝?
考虑以下场景:
const original = {
user: {
name: 'Alice',
hobbies: ['reading', 'swimming']
},
settings: {
darkMode: true
}
};
const shallowCopy = Object.assign({}, original);
shallowCopy.user.name = 'Bob';
shallowCopy.user.hobbies.push('running');
console.log(original.user.name); // 'Bob' - 被修改了!
console.log(original.user.hobbies); // ['reading', 'swimming', 'running'] - 也被修改了!
2.2 JSON方法实现深拷贝
最简单的深拷贝方法:
const deepCopy = JSON.parse(JSON.stringify(original));
局限性:
- 不能处理函数、Symbol、undefined
- 会丢失值为undefined的属性
- 不能处理循环引用
- 会破坏特殊对象如Date、RegExp
const problematicObj = {
date: new Date(),
fn: function() {},
sym: Symbol('id'),
undef: undefined,
infinity: Infinity,
// 循环引用
self: null
};
problematicObj.self = problematicObj;
const flawedCopy = JSON.parse(JSON.stringify(problematicObj));
console.log(flawedCopy);
// {
// date: "2023-05-15T12:00:00.000Z", // Date变成了字符串
// infinity: null, // Infinity变成了null
// self: null // 循环引用被破坏
// }
// 缺少fn、sym、undef
2.3 手写深拷贝实现
2.3.1 基础版本
function deepClone(source) {
if (typeof source !== 'object' || source === null) {
return source; // 基本类型直接返回
}
const target = Array.isArray(source) ? [] : {};
for (const key in source) {
if (source.hasOwnProperty(key)) {
target[key] = deepClone(source[key]);
}
}
return target;
}
2.3.2 处理循环引用
基础版本遇到循环引用会栈溢出:
const obj = { a: 1 };
obj.self = obj;
deepClone(obj); // 无限递归导致栈溢出
改进版本使用WeakMap存储已拷贝对象:
function deepClone(source, map = new WeakMap()) {
if (typeof source !== 'object' || source === null) {
return source;
}
// 检查是否已拷贝过
if (map.has(source)) {
return map.get(source);
}
const target = Array.isArray(source) ? [] : {};
map.set(source, target); // 记录拷贝关系
// 处理普通属性
for (const key in source) {
target[key] = deepClone(source[key], map);
}
return target;
}
2.4 性能优化考虑
深拷贝是昂贵的操作,特别是对于大型对象。优化策略包括:
- 循环检测:使用WeakMap避免无限递归
- 类型判断优化:使用更高效的类型检查方法
- 并行化:对于超大对象,可以考虑Web Worker
- 惰性拷贝:只在修改时拷贝(Copy-On-Write)
三、实战篇:应用场景与选择建议
3.1 何时使用浅拷贝?
- 对象结构简单,没有嵌套
- 只需要复制第一层属性
- 性能要求高,对象较大
- 明确知道嵌套对象不需要修改
3.2 何时使用深拷贝?
- 对象有复杂嵌套结构
- 需要完全隔离副本和原对象
- 需要修改嵌套属性而不影响原对象
- 不确定对象结构但需要安全操作
四、终极拷问:面试官想考察什么?
当面试官问及深浅拷贝时,他们通常希望考察:
- 基础概念:理解赋值、浅拷贝、深拷贝的区别
- 实现能力:能否手写深拷贝实现
- 问题意识:了解各种方法的局限性和边界情况
- 性能考量:对不同场景下的选择有合理判断
- 实践经验:在实际项目中的应用经验
结语
深浅拷贝就像是我们处理数据时的"复制粘贴"操作,理解它们的区别和适用场景,能帮助我们在开发中避免许多隐蔽的bug。记住:
- 浅拷贝是"我变你也变"的共享关系
- 深拷贝是"你变我不变"的独立关系
- 根据实际需求选择合适的方法
- 在性能和安全之间找到平衡
希望这篇笔记能帮助你彻底掌握这个看似简单实则深奥的概念!下次面试被问到深浅拷贝时,相信你一定能对答如流,让面试官眼前一亮!