浅拷贝与深拷贝:从入门到精通

124 阅读5分钟

前言:一个关于复印机的故事

想象一下,你有一份重要的文件需要复印。如果你使用的是普通复印机(浅拷贝),当你修改复印件上的内容时,原文件不会受到影响;但如果你修改的是复印件上粘着的便利贴(引用对象),那么原文件上的便利贴也会跟着改变。而深拷贝就像是一台神奇的复印机,它能完整复制文件以及上面所有的便利贴,并且修改复印件不会影响原件。

这就是编程中浅拷贝与深拷贝的核心区别。作为前端开发者,理解这个概念至关重要,因为它关系到我们如何处理数据,避免意外的副作用。

一、基础篇:认识拷贝

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 性能优化考虑

深拷贝是昂贵的操作,特别是对于大型对象。优化策略包括:

  1. 循环检测:使用WeakMap避免无限递归
  2. 类型判断优化:使用更高效的类型检查方法
  3. 并行化:对于超大对象,可以考虑Web Worker
  4. 惰性拷贝:只在修改时拷贝(Copy-On-Write)

三、实战篇:应用场景与选择建议

3.1 何时使用浅拷贝?

  1. 对象结构简单,没有嵌套
  2. 只需要复制第一层属性
  3. 性能要求高,对象较大
  4. 明确知道嵌套对象不需要修改

3.2 何时使用深拷贝?

  1. 对象有复杂嵌套结构
  2. 需要完全隔离副本和原对象
  3. 需要修改嵌套属性而不影响原对象
  4. 不确定对象结构但需要安全操作

四、终极拷问:面试官想考察什么?

当面试官问及深浅拷贝时,他们通常希望考察:

  1. 基础概念:理解赋值、浅拷贝、深拷贝的区别
  2. 实现能力:能否手写深拷贝实现
  3. 问题意识:了解各种方法的局限性和边界情况
  4. 性能考量:对不同场景下的选择有合理判断
  5. 实践经验:在实际项目中的应用经验

结语

深浅拷贝就像是我们处理数据时的"复制粘贴"操作,理解它们的区别和适用场景,能帮助我们在开发中避免许多隐蔽的bug。记住:

  • 浅拷贝是"我变你也变"的共享关系
  • 深拷贝是"你变我不变"的独立关系
  • 根据实际需求选择合适的方法
  • 在性能和安全之间找到平衡

希望这篇笔记能帮助你彻底掌握这个看似简单实则深奥的概念!下次面试被问到深浅拷贝时,相信你一定能对答如流,让面试官眼前一亮!