浅拷贝和深拷贝

1,456 阅读6分钟

前言

如果把拷贝比作恋爱,如果你是初尝禁果,浅谈恋爱,那么她对你的影响就很深了;但如果你是个master,也许转身就会有花香拂面,前任?是谁?在编程中,拷贝对象是一个常见的操作。也是面试中不可或缺的一道质量题,如果你是个女孩子可能会手下留情,否则,有的聊了,进可攻,退可守。这篇文章将深入探讨两者的区别和实现方法。

浅拷贝和深拷贝

在探讨之前,我们需要知道他们到底是什么

浅拷贝

浅拷贝指的是创建一个新对象,这个新对象只是简单地复制原对象的属性,但属性值仍然指向原对象中引用的内存地址。因此,浅拷贝后的对象会受到原对象的影响。

深拷贝

深拷贝指的是创建一个新对象,这个新对象的所有属性都是独立的副本,因此拷贝后的对象不会受到原对象的影响。

我们来看看这段代码

var a = 1;
var b = a;
a = 2;
console.log(a, b); // 2, 1 

var obj1 = {c: 1};
var obj2 = obj1;
obj1.c = 2;
console.log(obj1.c, obj2.c); // 2, 2  obj1、obj2都指向同一数据

对于引用类型呢,是存放在堆中的,在调用栈中的只是一个地址,就像obj1和obj2都指向同一个对象{c:1},当更改c的值时,两者都会受到影响,但有时候这并不是我们想要的,我们需要互不影响,各行其路。 image.png

image.png

浅拷贝的实现

1.Object.create(obj);

let obj={
    a:1
}
let obj2=Object.create(obj);
obj.a=2;
console.log(obj2.a);// 2

相信各位能够很轻松的写出这段代码,那么你还知道还有其他方法也能实现吗?

2.Object.assign(target, ...sources)

Object.assign() 方法用于将所有可枚举的自己的属性从一个或多个源对象复制到目标对象。它将返回目标对象。 是的,虽然它可以拷贝属性,但它似乎并没有创建一个对象,但是,如果我们这样呢?

let obj={
    age:18,
    b:[1,2]
}
let obj2=Object.assign({},obj)

这样改造一下我们也得以实现,我们来试试吧。

image.png 呃,好像它并没有改变,还没翻车,因为age是一个基本number类型,所以并不会有改变。

image.png 但如果是引用类型,我们可以看见是会随着原对象属性进行改变的。

3. Array.prototype.concat()

Array 实例 concat() 的方法用于合并两个或多个数组。此方法不会更改现有数组,而是返回一个新数组。

let arr = [1,2,3,4]
let arr1 = [1,2,3,4,{a:1}]
let arr2= [].concat(arr) 
let arr3= [].concat(arr1) 
console.log(arr2); // [ 1, 2, 3, 4 ]
arr.push(5)
arr1[4].a = 2
console.log(arr2); // [  1, 2, 3, 4 ] 
console.log(arr3[4].a) ; //2 

我们可以看见,如果我们对原数组push一个值arr.push(5),arr2是不会改变的,这和上面的assign方法是类似的。其实Array.prototype.slice()也是有同样的深拷贝的假象

4.展开运算符...也可以实现

展开运算符是一个 es6 / es2015特性,它提供了一种非常方便的方式来执行浅拷贝,这与 Object.assign ()的功能相同。

let obj1 = { name: 'Kobe', number:{a:8,b:24}}
let obj2= {... obj1}
obj1.number.a = 30;
obj1.name = 'curry'
console.log(obj2) 

image.png

5.arr.toReversed().reverse()

toReversed() 方法是 reverse() 方法的复制对应项。它返回一个新数组,其中元素的顺序相反。 reverse() 方法就地反转数组并返回对同一数组的引用。因此 我们结合两个方法也可以实现浅拷贝。

手捏一个浅拷贝

看了这么多猪跑,吃吃猪肉不过分吧。

不知道大家有没有发现一个细节,在举例 Object.assign(target, ...sources)的时候强调了可枚举的,自己的

  • 自己的属性:对象本身定义的属性,不包括从原型链上继承的属性。

  • 可枚举属性:通过 for...in 循环或 Object.keys 方法可以枚举的属性。默认情况下,大部分属性是可枚举的,除非它们的可枚举性被显式设置为 false

  • hasOwnProperty() 方法返回一个布尔值,指示该对象是否将指定属性作为其自己的属性(而不是继承它)。

  • Object.hasOwn() 如果指定对象将指示的属性作为其自己的属性,则 Object.hasOwn() 静态方法返回 true 。如果该属性是继承的或不存在,则该方法返回 false 。

  • 推荐使用Object.hasOwn()而不是 Object.prototype.hasOwnProperty() ,因为它适用于 null 原型对象以及已重写继承的 hasOwnProperty() 方法的对象

所以说,如果是原型链上继承的属性for...in(循环遍历对象属性)是不应该被拷贝的。

function shallowCopy(obj) {
    let newObj = {}
    // 如果对象隐式原型有很多属性,往往都是不必要的 我们应该想办法忽略

    for (let key in obj) {
        if (Object.hasOwn(obj,key)) {
            newObj[key] = obj[key]
        }
    }
    return newObj;
}

let obj = {
    a: 1,
    b: { n: 2 }
}
let newObj = shallowCopy(obj)
obj.a=2
obj.b.n = 4
console.log(newObj);//{ a: 1, b: { n: 4 } }

深拷贝

有时候我们更向往自由,不喜欢有线牵连着。实现深拷贝的方法也很轻松很直接。

JSON.parse(JSON.stringify(source))

let obj={
    a:1,
    b:{n:2}
}
// let s=JSON.stringify(obj)//{"a":1,"b":{"n":2}}  将对象转换成JSON格式的字符串
// let obj2=JSON.parse(s)//{ a: 1, b: { n: 2 } } 将JSON格式的字符串转换成对象


function cloneJSON(source) {
    return JSON.parse(JSON.stringify(source));
}
let obj={
    a:1,
    b:{n:2},
    c:'ccc',
    d:true,
    e:null,
    f:undefined,
    g:[1,2,3],
    h:function(){},
    i:Symbol(1)
}

let newObj=cloneJSON(obj)
console.log(newObj);

image.png 一行代码就搞定,但我们可以看见,它无法识别bigInt类型、无法拷贝 undefined 、function、Symbol 、无法处理循环引用 example:obj.a=obj.b obj.b.m=obj.a。

structuredClon()

近年JS新增了 structuredClon()方法,全局 structuredClone() 方法使用结构化克隆算法创建给定值的深度克隆。 该方法还允许将原始值中的可转移对象转移而不是克隆到新对象。转移的对象与原始对象分离并附加到新对象;它们在原始对象中不再可访问。

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

}
const newObj=structuredClone(obj);
obj.b.n=3
console.log(newObj);// { a: 1, b: { n: 2 } } 

手写深拷贝

let obj = {
    a: 1,
    b: { n: 2 }
}
function deepCopy(obj) {
    let newObj = {}
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {//规避隐式原型属性
            //判断是不是对象
            if (obj[key] instanceof Object) {
                //typeof obj[key]==='object' && obj[key]!==bull
                newObj[key] = deepCopy(obj[key])
            }
            else {
                newObj[key] = obj[key]
            }
        }
    }
    return newObj
}
let obj2 = deepCopy(obj)
obj.b.n = 6
console.log(obj2);//{ a: 1, b: { n: 2 } }

思路讲解

  1. 初始化新对象:首先创建一个新的空对象 newObj,它将是深拷贝后的对象。

  2. 遍历原对象:使用 for...in 循环遍历原对象的所有可枚举属性。

  3. 过滤继承属性:使用 obj.hasOwnProperty(key) 来过滤掉从原型链上继承的属性,只处理原对象自身的属性。

  4. 判断属性类型

    • 如果属性是对象(注意这里用 typeof obj[key] === 'object' && obj[key] !== null判断对象,为了我们代码更优雅,更好的做法是 instanceof Object ),则递归调用 deepCopy 进行深拷贝。
    • 如果属性不是对象,则直接复制属性值。
  5. 返回新对象:当所有属性都处理完毕后,返回新对象 newObj

当然,官方更推荐我们使用Object.hasOwn()来判断该属性是否为自己的属性而不是原型链上继承的属性,大家可以试试再进行修改优化。

这样,我们就完成了深拷贝的实现,如果你有更好的想法,欢迎留言交流