每日一学:JS中的拷贝

170 阅读9分钟

前言

在js中,拷贝起着很重要的作用,它不仅可以保护原始数据避免被修改,还有助于避免引用类型数据操作中的常见陷阱,还支持数据的灵活复用与变异,适配不同场景下的性能与安全需求。无论是通过浅拷贝实现快速的结构复制,还是借助深拷贝达到完全独立的数据隔离,拷贝都是实现高效、可靠代码的基础,增强了程序的可控性、稳定性和可维护性。接下来,就由我来讲述一下我关于拷贝的一些学习心得。

拷贝

拷贝通常只在引用类型上讨论,因为原始属性上的拷贝很简单,直接令b=a即可,这是一个简单的深拷贝,而引用类型的拷贝则要复杂的多。

拷贝分为了浅拷贝深拷贝浅拷贝的结果受原对象的影响,深拷贝的结果不受原对象的影响,下面我们来细讲。

浅拷贝

浅拷贝只会复制对象的第一层属性,如果属性是引用类型(如对象、数组),则只会复制它们的引用地址,而不是创建一个新的实例,这就要细讲一下应用地址了:

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

在这样一段代码的执行要先经过预编译,然后执行,而引用类型是无法进入调用栈的,而是在原地留下一个引用地址,然后自身进入中,让js执行引擎去中找,而其实就是js中负责存放引用类型的地方,就像这样:

image.png

在js这门语言中,有很多内置方法可以实现浅拷贝,让我来一一介绍:

1.Object.create(obj)

这个方法严格意义上并不能算是一个浅拷贝,但有浅拷贝有的特点,即可以创建一个新的对象,这个新对象的原型(__proto__)被设置为obj。这意味着新创建的对象会继承obj的所有可继承属性和方法,这种方式主要用于实现基于原型的继承。

与直接复制或克隆一个对象不同,Object.create(obj)强调的是原型链的继承关系,而非生成一个包含相同属性值的新对象。因此,当你修改新对象的属性时,不会影响到作为原型的obj,但如果你访问一个新对象上未定义的属性,JavaScript引擎会沿着原型链查找,可能会找到obj上的属性。

通过这个方法拷贝创建的对象,它拷贝原对象的属性存储在它的隐式原型上,而且受原对象的影响,下面我来给出一个实例:

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

它的执行结果是这样的:

image.png

而这时我们去看下obj2是什么样的:

image.png

我们可以看到a是存储在了obj2的隐式原型上,而且由obj去改动a的值,obj2上也是有效果的。

虽然这严格意义上不算浅拷贝,但效果是差不多的,于是我就算作浅拷贝了。

2.Object.assign({}, obj)

Object.assign(a,b)实际上是Object()这个js内置对象构造函数的一个内置方法的调用,而这个方法的效果就是把b这个对象合并到a中去,这时的a会多出b中的属性,而b没有影响。这时我们把a换成空对象,不就等于得到了b的拷贝体了吗,现在的问题就是这样的方法究竟是浅拷贝还是深拷贝呢?下面我来给出一个实例:

let obj = {
    a:1,
    b:{n:2}
}
let obj2 = Object.assign({}, obj)
obj.a=2
obj.b.n = 1
console.log(obj2);

这是它的结果:

image.png

这么一看,a的值没有改变,而b的n值却改变了,这样是算浅拷贝还是深拷贝呢?当然是浅拷贝,这不是受到了原对象的影响了吗,而深拷贝是不会受到原对象的影响的,讨论完这个问题,又有一个问题,为什么a不受影响而b受影响?

其实它的原理是这样的:

当我们调用这个方法时,它会去obj的引用地址上去把obj的属性拷贝下来,而a就直接被拷贝了,而b也是直接被拷贝了,但b存储的是引用地址,引用地址被拷贝到obj2上,那么objobj2b指向不就是同一个了吗,那么去改objb就是在改obj2上的b

这就是对象上的浅拷贝了。

3.[].concat(arr)

数组上也有一个合并的方法,a.concat(b)b合并到a中,而这也是一个浅拷贝,原理与对象合并几乎一致,我就不多赘述了。

4.let newarr = [...arr]

...arr是解构arr这个数组,而[...arr]是把arr解构出的元素又重新构成了一个数组,而这其实也是一样,把原始类型直接写入,而引用类型则是传了引用地址,导致引用类型受原数组的影响。

5.arr.slice(0)

arr.slice(1,3)表示将arr这个数组从下标1开始3结束拓印一份这些元素,而不影响arr,那么arr.slice(0)则表示拓印整个数组,而不影响arr,而这也是一个与前面类似的浅拷贝,这也不难理解,你引用类型在js中的特殊地位,导致元素存的都是引用地址,导致我拷贝的也是引用地址,那么只要原对象(数组在js里其实也是对象)有引用类型就都能影响拷贝后的对象,那就都叫浅拷贝。

6.arr.toReversed.reverse()

toReversed这个方法在数组中是把一个数组颠倒后存进一个新数组中,不影响原数组,reverse这个方法是把一个数组直接颠倒过来,会影响原数组,那么就有了arr.toReversed.reverse(),这个可以将arr颠倒后存入新数组在颠倒回来,这就得到了一个与原数组一样的数组,这也是浅拷贝,这也是拷贝引用地址而被原数组影响。

至此,js中内置的创建浅拷贝的方法就是这些,而手写一个浅拷贝在看完这些,也是轻而易举,这就来写一个:

手写浅拷贝

在写代码之前,我们来整理一下思绪,来想一下浅拷贝到底怎么写。

首先,我们需要一个新的对象

直接创建{}就行

然后,我们需要获取原对象上的属性

for...in...循环这个js提供的遍历对象内的key的方法帮我们解决了这个难题

还有,我们不需要拷贝原对象隐式具有的属性,而for...in...循环会拷贝隐式具有的属性

hasOwnProperty(key)也是js提供的判断对象内key是否是显式具有的属性,是则true,否则false

万事俱备,只欠代码:

function shallowCopy(obj){
    let copy={}
    for(var key in obj){
        // key是不是obj显示具有的
        if(obj.hasOwnProperty(key)){
            copy[key] = obj[key]
        }
    }
    return copy;
}

效果如下:

image.png

这就是一份手写的浅拷贝,十分的简单,所以面试官不常考。接下来是深拷贝。

深拷贝

不像浅拷贝,目前为止,js内部只有两个方法可以完成深拷贝,其中一个也是最近才加上的。但这并不是说深拷贝很难,与浅拷贝相比,其实就多了一些步骤,即对对象内部的属性进行识别,识别到引用类型,就新建一个,再往里识别,这样循环添加就可以了。

JSON.parse(JSON.stringify(obj))

在js老版本只内置了一个方法可以完成深拷贝,即:JSON.parse(JSON.stringify(obj))方法,这个方法可以返回一个拷贝后对象。JSON.stringify(obj)方法是JSON对象内置的一个方法,可以将对象转换为JSON字符串且不影响原对象,而JSON.parse()又可以将这个字符串转换为对象,但这个方法有几个缺陷就是

1. 无法识别bigInt类型

2. 无法拷贝 undefined、function、Symbol类型

3. 无法处理循环引用

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

// obj.a = obj.b
// obj.b.m = obj.a
let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj);

结果如下:

image.png

structuredClone(obj)

这是22年左右js更新的一个方法,可以进行深拷贝,但依旧无法拷贝function以及Symbol类型

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

const newObj = structuredClone(obj)
obj.b.n = 10
console.log(newObj);

结果如下:

image.png 这就是js内部的深拷贝实现方法,下面我们来手写一个深拷贝

手写深拷贝

与浅拷贝类似:

1. for..in 遍历原对象上的属性

2. 用hasOwnProperty(key)规避对象上隐式具有的属性

3. 如果key的值是一个对象,则新创建一个对象来继续递归deepClone(),直到找到原始值结束并返回结果

深拷贝与浅拷贝的区别就在第三点,浅拷贝在原对象内找到一个对象时是直接记录引用地址,而深拷贝就需要创建一个对象来记录这个对象里的属性

这样代码也水落石出了:

function deepCopy(obj){
    let newObj ={}
    for(let key in obj){
        if(obj.hasOwnProperty(key)){
            if(obj[key] instanceof Object){
                newObj[key] = deepCopy(obj[key])
            }else{
                newObj[key] = obj[key]
            }
        }
    }
    return newObj
}

当完成前两步后,会进入if判断,判断是否是对象类型(a instanceof XX类型,js内置的判断a是否属于XX类型的关键字,是则返回true,否则返回false),是就进入递归,再进行前两步,直到找到原始类型,刻录后返回给newobj[key],这样做是防止属性内的对象嵌套多层;不是就进入else,直接拿给newobj[key]。 效果如下:

image.png 深拷贝就结束了。

结语

这是js学习旅程的又一难点,(其实也不是很难),希望这篇文章能对你有所帮助,我是Ace,我们下次再见!!!