js拷贝

128 阅读5分钟

今天来学习一下JS中的拷贝:

let obj = {
    name: 'Ro9nin'
}
let obj2 = obj
obj.name = 'Ro9n1n'

console.log(obj2.name);

上面的let obj2 = obj这一操作不能叫拷贝。因为这个操作只是让obj2 只是获得了 obj引用地址,这意味着 obj2obj 指向内存中的同一个对象obj2并未创建一个和obj一样的新对象;当对象obj身上的name改变时,因为二者的引用地址一样obj2.name也会跟着变化。

image.png

所以,在JavaScript中,根据拷贝的层次,可以分为两种类型:浅拷贝深拷贝。接下来我们就来细聊它们的区别以及实现的方式:

浅拷贝

浅拷贝 创建一个新对象,并将原始对象中的所有非引用类型属性值复制到新对象中。对于引用类型属性,它只复制了指向原始对象的引用,而不是复制引用所指向的实际数据。这意味着如果原始对象和浅拷贝的对象都引用了同一个数组或对象,那么对其中一个对象的修改会影响到另一个对象。所以浅拷贝后的新对象会受到原对象的影响而改变;

js中有哪些方法可以实现浅拷贝呢?

    1. Object.create(obj)

在js中,Object.create() 用于创建一个新对象,同时指定新创建的对象的原型.

let obj = {
a : 2
}
let newObj = Object.create(obj)

console.log(newObj);  
console.log(newObj.a);

这个例子中,使用Object.create(obj)创建了一个新对象newObj,并把它的原型(prototype)设置为obj;这意味着新创建的对象newObj会继承obj的中的a,使用newObj.a同样能访问到其原型上的a的值。

但使用 Object.create(obj) 的方式不算真正意义上的拷贝 ,这个方法主要是用来实现原型继承,使得新对象可以访问到obj上的属性和方法。

    1. [].concat(arr)

[].concat(arr) 是js中用于将数组 arr 连接到一个新数组中的方法。它会返回一个新数组,其中包含原始数组和 arr 中的所有元素。

let arr = [1,2,3,{a:1}]
let arr2 = [].concat(arr)

arr[3].a = 2

console.log(arr2);

输出结果为 [1,2,3,{a:2}]

    1. Object.assign({},obj)

在JavaScript中,assign() 方法是 Object 构造函数的一个静态方法,用于将一个或多个源对象的可枚举属性的值复制到目标对象中。此方法会在目标对象上进行就地修改,并返回修改后的目标对象。

let obj = {
    a: 1,
    b: [1,2]
}

let obj2 = Object.assign({},obj) 
obj.b.push(3)

console.log(obj2.b);

输出结果为[1,2,3]

利用这一特性,我们可以通过Object.assign({},obj)来完成浅拷贝的操作;将目标对象里的属性都复制到一个空对象中。这些属性在新对象中仍然是通过引用共享的,且会被原对象影响。

    1. arr.slice(0)

在js中,数组身上的slice方法,是用于从数组中提取一个新的子数组的方法

let arr = [1,2,3,{a:1}]
let arr2 = arr.slice(0)

arr[3].a = 2

console.log(arr2);

输出结果为 [1,2,3,{a:2}]

    1. 数组解构 ...

在数组中,也可以通过解构的方法,实现浅拷贝;在js中解构数组用...表示

let arr = [1,2,3,{a:1}]
let arr2 = [...arr] // 将原数组元素解构到新的空数组当中

arr[3].a = 2

console.log(arr2);

输出结果为 [1,2,3,{a:2}]

  • 手写方法实现浅拷贝

浅拷贝的原理,即是创建一个副本对象,并把原对象的原始类型值复制过来,而引用类型则是复制其引用地址。在知道原理后,手动写一个实现浅拷贝的方法也很简单:

let obj = {
    a: 1,
    b: {n: 1}
}

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

let obj2 = clone(obj)
obj.b.n=2
console.log(obj2);

image.png

在这个示例中,clone函数实现的是一个浅拷贝。虽然newobj包含了obj的属性ab,但是当b是一个对象时,newobj.b只是指向了obj.b的同一引用,而不是创建了一个新的独立对象。

因此,当执行obj.b.n = 2时,实际上改变了objnewobj共享的b对象的属性n。结果是,当打印obj2时,将看到b对象的n属性已经被更新为2

深拷贝

深拷贝不仅复制对象本身,还递归地复制对象内的所有引用类型。这意味着新对象及其内部的引用类型数据都是完全独立的,修改其中任何一个都不会影响到其他对象。

在js中有以下方法可以实现深拷贝:

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

在js中,JSON.parse(JSON.stringify(obj)) 是一种常用的深拷贝对象的方法

let obj = { 
a: 1, 
b: {c: 2 } 
}; 
let newObj = JSON.parse(JSON.stringify(obj)); 
newObj.a = 3; 
newObj.b.c = 4; 
console.log(obj.a);  
console.log(obj.b.c); 

结果为 1 , 2 可以看到,当修改 newObj 的属性时,原始对象 obj 的属性并没有受到影响,这表明深拷贝成功实现了对象的完全独立复制。

    1. structuredClone()

structuredClone() 提供了一种创建对象、数组以及一些特殊类型值的深拷贝的方法

let obj = {
    a: 1,
    b: {n: 2},
    c: 'DAMN',
    d: true,
    e: undefined,
    f: null,
    // g: function(){},
    // h: Symbol(1),
    i: 123n
}


const newObj = structuredClone(obj)
obj.b.n = 20

console.log(newObj);

image.png

在上面的执行结果中,可以看到structuredClone() 方法可以实现深拷贝。

  • 手写方法实现深拷贝
Object.prototype.c = 3
let obj = {
    a: 1,
    b: {n: 1}
}

function deepClone(obj) {
    let newObj = {}
    for (let key in obj){
        if(obj.hasOwnProperty(key)){
            if(typeof obj[key] === 'object' && obj[key] !== null){
                newObj[key] = deepClone(obj[key]);
            } else {
            newObj[key] = obj[key]
                    }
    }
}
return newObj
}

let obj3 = deepClone(obj)
obj.b.n=2
console.log(obj,obj3);

image.png

deepClone函数实现了一个基本的深拷贝,但是它只检查了obj对象自身的属性,没有考虑原型链。这意味着newObj将不会包含从Object.prototype继承的c属性。

关于obj.b.n = 2之后的行为,deepClone正确地创建了obj的一个深拷贝,即obj3。因此,修改obj.b.n不会影响obj3.b.n的值,因为b对象也被深拷贝了。