JavaScript 深浅拷贝全解析:从方法到应用

114 阅读5分钟

在 JavaScript 的编程世界里,数据的复制操作就像一场精心设计的 “数据分身术”。当我们希望创建一个与原对象外观相同的副本时,拷贝操作便登场了。不过,根据对数据处理的深度不同,拷贝又分为浅拷贝和深拷贝,它们各自有着独特的实现方法与应用场景。

一、拷贝的基本概念:复刻对象的 “分身术”

拷贝,简单来说就是复刻一个对象,使其与原对象看起来一模一样。在 JavaScript 中,基于数据类型的不同,拷贝的表现和实现方式也有所差异。基本数据类型(如数字、字符串、布尔值等)的拷贝较为直接,新变量会拥有独立于原变量的值;而引用数据类型(对象、数组等)的拷贝则相对复杂,涉及到内存地址的处理,这也正是浅拷贝和深拷贝概念产生的根源。

二、浅拷贝:表层的 “镜像复制”

浅拷贝,就如同给对象拍摄一张 “表层快照”,它仅仅复制对象最外层的属性。对于引用数据类型的属性,浅拷贝不会深入复制其内部结构,而是让新对象和原对象共享这些属性的内存地址。这就意味着,如果原对象中引用数据类型的属性发生改变,新对象中对应的属性也会随之改变。

浅拷贝的实现方法

1. Object.create(obj)

通过该方法创建的新对象,其原型会指向传入的obj对象,同时会将obj对象自身的可枚举属性复制到新对象上。这是一种基于原型链的浅拷贝方式,例如:

const original = { a: 1, b: { c: 2 } };
const copy = Object.create(original);

original.b.c = 3;
console.log(copy.b.c); // 输出 3

2. [].concat(arr)

对于数组,concat方法可以将传入的数组连接到原数组,并返回一个新数组,实现浅拷贝。它会将原数组的元素复制到新数组中,但对于数组中的对象元素,只是复制引用

const arr1 = [1, { x: 10 }];
const arr2 = [].concat(arr1);
arr1[1].x = 20;
console.log(arr2[1].x); // 输出 20

3. 数组解构[...arr]

使用展开运算符进行数组解构,也能快速实现浅拷贝。新数组会拥有与原数组相同的元素,但同样对于对象元素仅复制引用

const arr3 = [2, { y: 30 }];
const arr4 = [...arr3];
arr3[1].y = 40;
console.log(arr4[1].y); // 输出 40

4. arr.slice(0, arr.length)

slice方法可以截取数组的一部分或全部元素,当传入0和数组长度时,会返回一个包含原数组所有元素的新数组,实现浅拷贝:

const arr5 = [3, { z: 50 }];
const arr6 = arr5.slice(0, arr5.length);
arr5[1].z = 60;
console.log(arr6[1].z); // 输出 60

5. Object.assign({}, obj)

该方法可以将一个或多个源对象的属性复制到目标对象中,常用于对象的浅拷贝。它会将源对象的属性逐一复制到新创建的空对象上,但对于对象属性同样是浅复制

const obj1 = { p: 1, q: { r: 2 } };
const obj2 = Object.assign({}, obj1);
obj1.q.r = 3;
console.log(obj2.q.r); // 输出 3

6. arr.toReversed().reverse()

toReversed方法会返回一个新的反转后的数组,再通过reverse方法还原顺序,也能实现数组的浅拷贝,同样存在对对象元素浅复制的情况

const arr7 = [4, { w: 70 }];
const arr8 = arr7.toReversed().reverse();
arr7[1].w = 80;
console.log(arr8[1].w); // 输出 80

三、深拷贝:深层的 “独立复刻”

深拷贝追求的是创建一个与原对象完全独立的副本,它会递归地遍历对象的每一层属性,对于引用数据类型的属性,也会深入复制其内部结构,确保新对象和原对象在内存中完全分离。这样一来,无论原对象如何变化,新对象都不会受到影响。

深拷贝的实现方法

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

这是一种较为简单的深拷贝方式,通过将对象转换为 JSON 字符串,再解析回对象来实现。但它存在明显的局限性,无法识别bigint类型,无法处理undefinedsymbolfunction,并且也无法处理循环引用的情况。例如:

const originalObj = { 
    num: 1n, 
    func: () => {},
    sym: Symbol('test'), 
    undef: undefined 
};


const copiedObj = JSON.parse(JSON.stringify(originalObj));

console.log(copiedObj.num); // 输出 null
console.log(copiedObj.func); // 输出 undefined
console.log(copiedObj.sym); // 输出 undefined
console.log(copiedObj.undef); // 输出 undefined

2. structuredClone(obj)

这是现代 JavaScript 提供的一个专门用于深拷贝的方法,它能够处理更多复杂的数据类型,包括functionundefined等,并且可以处理循环引用。不过,它在一些旧版本的浏览器中可能不被支持

const complexObj = { a: 1, b: [2, 3], func: () => {} };
const clonedObj = structuredClone(complexObj);
complexObj.b[0] = 4;
console.log(clonedObj.b[0]); // 输出 2

四. 手写深浅拷贝

对对象这个数据类型实现

浅拷贝

只拷贝一层

function shallowCopy(obj){
    let newObj = {}
    for(let key in obj){
        if(obj.hasOwnProperty(key)){
            newObj[key]= obj[key]
        }
    }
    return newObj
}

深拷贝

递归拷贝每一层


function deepCopy(obj){
    let newObj = {}
    for(let key in obj){
        if(obj.hasOwnProperty(key)){
            if(typeof obj[key] === 'object'&&  obj[key]!==null){
                newObj[key] = deepCopy(obj[key])//递归拷贝引用类型的每一层
            }else{
                newObj[key] = obj[key]
            }
        }
    }
    return newObj;
}

五、深浅拷贝的应用场景

在实际开发中,选择浅拷贝还是深拷贝,需要根据具体的业务需求来决定。浅拷贝由于操作简单、性能较高,适用于对数据结构要求不高,或者数据结构较为简单,不存在深层嵌套引用数据类型的场景,比如快速复制一个简单的配置对象。而深拷贝则适用于需要完全隔离数据,防止数据之间相互干扰的场景,例如在处理复杂的状态管理、数据缓存,或者进行数据的备份和恢复时。

总之,深入理解 JavaScript 的深浅拷贝,掌握它们的实现方法与应用场景,能够帮助我们在编程过程中更加灵活、准确地处理数据,编写出更健壮、高效的代码。无论是面对简单的数据复制,还是复杂的数据结构处理,都能游刃有余地选择合适的拷贝方式,让程序运行得更加稳定可靠。