JS-深度解构 JavaScript 浅拷贝与深拷贝(附手写源码)

86 阅读3分钟

前言

在处理对象或数组时,我们经常遇到“改了 A,结果 B 也变了”的情况。这背后涉及到了内存地址、引用传递以及拷贝的深度问题。理解浅拷贝与深拷贝,是处理复杂数据流和状态管理(如 Redux, Vuex)的基础。

一、 核心概念对比

1. 引用赋值

直接使用 =,这不属于拷贝,只是让两个变量指向内存中的同一个地址

let obj1 = { a: 1 };
let obj2 = obj1; // 引用赋值
obj2.a = 2;
console.log(obj1.a); // 2 (相互影响)

2. 浅拷贝 (Shallow Copy)

创建一个新对象,拷贝其第一层属性。如果属性是基本类型,拷贝的是值;如果属性是引用类型,拷贝的是内存地址

  • 实现方式Object.assign()、扩展运算符 ...Array.prototype.slice()

3. 深拷贝 (Deep Copy)

创建一个新对象,递归地拷贝所有层级的属性。两个对象在内存中完全独立,互不影响。


二、 深拷贝的两种主流实现

方法 1:JSON 序列化(简单但有陷阱)

思路JSON.parse(JSON.stringify(obj))

  • 优点:简单快捷,一行代码搞定。

  • 缺点(面试常考点)

    1. 会忽略 undefinedsymbol
    2. 会忽略 function(函数无法被序列化)。
    3. Date 对象会变成字符串。
    4. 无法处理循环引用的对象(会报错)。
const obj1 = { 
    body: { a: 10 },
    say: function(){ console.log('hello') } 
};
const obj2 = JSON.parse(JSON.stringify(obj1));

console.log(obj2.say); // undefined (函数丢失了!)

方法 2:递归手动实现(面试必考)

要写出一个完美的 deepClone,需要考虑:数组兼容、递归调用、原型属性过滤

/**
 * 深拷贝递归实现
 * @param {Object} target 目标对象
 * @returns {Object}
 */
function deepClone(target) {
    // 1. 如果不是对象或者是 null,直接返回
    if (typeof target !== 'object' || target === null) {
        return target;
    }

    // 2. 初始化返回结果(判断是数组还是对象)
    const result = Array.isArray(target) ? [] : {};

    // 3. 遍历目标对象
    for (let key in target) {
        // 4. 确保只遍历对象自身的属性,不包含原型链
        if (target.hasOwnProperty(key)) {
            // 5. 如果子属性还是对象,递归调用
            if (target[key] && typeof target[key] === 'object') {
                result[key] = deepClone(target[key]);
            } else {
                // 6. 基本类型则直接赋值
                result[key] = target[key];
            }
        }
    }
    return result;
}

// 测试
const original = { name: 'ouyang', arr: [1, 2], fn: () => {} };
const cloned = deepClone(original);
cloned.arr[0] = 999;

console.log(original.arr[0]); // 1 (互不影响,成功!)

三、 面试模拟题

Q1:Object.assign() 是深拷贝还是浅拷贝?

参考回答:浅拷贝。它只拷贝源对象自身的可枚举属性到目标对象。如果源对象的属性值是一个指向对象的引用,它也只拷贝那个引用地址。

Q2:如何解决深拷贝中的“循环引用”问题?

参考回答: 在递归实现中,可以使用一个 WeakMap 来存储已经拷贝过的对象。每次拷贝前先在 WeakMap 中查找,如果已经存在,则直接返回该对象,避免死循环。

Q3:为什么 JSON.stringify 无法拷贝函数?

参考回答: 因为 JSON 是一种数据交换格式,其标准中只定义了数字、字符串、布尔值、数组、对象和 null。函数属于代码逻辑而非数据,因此在 JSON 序列化规范中被排除在外。


四、 总结:我该选哪种方案?

  1. 处理简单的纯数据:用 JSON.parse(JSON.stringify(obj)),效率最高。
  2. 处理包含函数或特殊对象的复杂数据:使用手写的 deepClone 或者成熟的第三方库如 Lodash_.cloneDeep()
  3. 高性能需求:如果数据量极大且只有一层,优先使用扩展运算符 {...obj}