深入理解 JavaScript 中的浅拷贝与深拷贝

146 阅读3分钟

前言

在 JavaScript 开发中,复制对象是一项常见的操作,通常分为浅拷贝(Shallow Copy)和深拷贝(Deep Copy)两种。理解这两种操作的区别和实现方法不仅对日常开发非常重要,还是面试中的高频考点。本篇文章将深入解析浅拷贝与深拷贝的核心概念,并展示几种常见的实现方法,帮助你更好地掌握它们。

浅拷贝与深拷贝的基本概念

浅拷贝:浅拷贝只复制对象的第一层属性。如果属性是基本类型,拷贝的是值本身;如果属性是引用类型,拷贝的则是引用地址。也就是说,浅拷贝后的新对象与原对象共享同一块内存空间,修改新对象的嵌套属性将影响到原对象。

深拷贝:深拷贝则会递归复制对象的所有层级,确保新对象和原对象完全独立。深拷贝后的新对象与原对象之间不会共享任何内存空间,修改新对象不会影响到原对象。

理解浅拷贝

浅拷贝只复制对象的第一层属性,如果对象中包含嵌套的引用类型(如对象、数组),浅拷贝只会复制引用地址,而不会复制引用对象本身。

示例代码

let obj1 = { name: '张三', details: { age: 25, address: '上海' } };
let obj2 = { ...obj1 };
obj2.details.age = 30;

console.log(obj1.details.age); // 30
console.log(obj2.details.age); // 30

在这个例子中,obj2obj1 的浅拷贝,由于 details 是一个引用类型,它们指向同一块内存。因此,修改 obj2.details.age 也会影响到 obj1.details.age

常见的浅拷贝实现方法

  1. Object.assign()

    let obj1 = { name: '张三', details: { age: 25, address: '上海' } };
    let obj2 = Object.assign({}, obj1);
    obj2.details.age = 30;
    console.log(obj1.details.age); // 30
    
  2. 扩展运算符 ...

    let obj1 = { name: '张三', details: { age: 25, address: '上海' } };
    let obj2 = { ...obj1 };
    obj2.details.age = 30;
    console.log(obj1.details.age); // 30
    
  3. Array.prototype.concat()

    let arr1 = [1, 2, { name: '张三' }];
    let arr2 = arr1.concat();
    arr2[2].name = '李四';
    console.log(arr1[2].name); // 李四
    
  4. Array.prototype.slice()

    let arr1 = [1, 2, { name: '张三' }];
    let arr2 = arr1.slice();
    arr2[2].name = '李四';
    console.log(arr1[2].name); // 李四
    

理解深拷贝

深拷贝会递归地复制对象的每一层属性,确保新对象与原对象之间完全独立。无论对象有多深层嵌套,深拷贝都会完整地复制整个结构。

示例代码

let obj1 = { name: '张三', details: { age: 25, address: '上海' } };
let obj3 = deepClone(obj1);
obj3.details.age = 30;

console.log(obj1.details.age); // 25
console.log(obj3.details.age); // 30

function deepClone(obj) {
    if (obj === null) return null;
    if (typeof obj !== 'object') return obj;
    let clone = Array.isArray(obj) ? [] : {};
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            clone[key] = deepClone(obj[key]);
        }
    }
    return clone;
}

在这个例子中,obj3obj1 的深拷贝,修改 obj3.details.age 并不会影响 obj1.details.age,因为它们是完全独立的对象。

常见的深拷贝实现方法

  1. JSON.parse(JSON.stringify())

    let obj1 = { name: '张三', details: { age: 25, address: '上海' } };
    let obj2 = JSON.parse(JSON.stringify(obj1));
    obj2.details.age = 30;
    
    console.log(obj1.details.age); // 25
    console.log(obj2.details.age); // 30
    

    注意:此方法不能处理函数、undefined、循环引用和复杂对象(如 DateRegExp)。

  2. Lodash 的 _.cloneDeep()

    let _ = require('lodash');
    let obj1 = { name: '张三', details: { age: 25, address: '上海' } };
    let obj2 = _.cloneDeep(obj1);
    obj2.details.age = 30;
    
    console.log(obj1.details.age); // 25
    console.log(obj2.details.age); // 30
    
  3. jQuery.extend(true, {})

    let $ = require('jquery');
    let obj1 = { name: '张三', details: { age: 25, address: '上海' } };
    let obj2 = $.extend(true, {}, obj1);
    obj2.details.age = 30;
    
    console.log(obj1.details.age); // 25
    console.log(obj2.details.age); // 30
    

总结

浅拷贝和深拷贝是 JavaScript 中对象复制的两种关键方式,各自有着不同的应用场景。浅拷贝 复制对象的第一层属性,而对于引用类型的属性,拷贝的是内存地址,因此新旧对象仍然共享相同的引用。深拷贝 则会递归复制对象的所有层级,确保每个属性都独立存在,不再共享内存空间。

掌握这两种拷贝方式可以帮助你在开发中避免意外的数据修改和潜在的 bug,尤其是在处理复杂对象时。了解不同的实现方法和它们的优缺点,你可以根据具体需求选择最适合的复制方式,确保代码的稳定性和可靠性。