前言
在js中,拷贝起着很重要的作用,它不仅可以保护原始数据避免被修改,还有助于避免引用类型数据操作中的常见陷阱,还支持数据的灵活复用与变异,适配不同场景下的性能与安全需求。无论是通过浅拷贝实现快速的结构复制,还是借助深拷贝达到完全独立的数据隔离,拷贝都是实现高效、可靠代码的基础,增强了程序的可控性、稳定性和可维护性。接下来,就由我来讲述一下我关于拷贝的一些学习心得。
拷贝
拷贝通常只在引用类型上讨论,因为原始属性上的拷贝很简单,直接令b=a即可,这是一个简单的深拷贝,而引用类型的拷贝则要复杂的多。
拷贝分为了浅拷贝和深拷贝。浅拷贝的结果受原对象的影响,深拷贝的结果不受原对象的影响,下面我们来细讲。
浅拷贝
浅拷贝只会复制对象的第一层属性,如果属性是引用类型(如对象、数组),则只会复制它们的引用地址,而不是创建一个新的实例,这就要细讲一下应用地址了:
let obj = {
a:1
b:{n:1}
}
在这样一段代码的执行要先经过预编译,然后执行,而引用类型是无法进入调用栈的,而是在原地留下一个引用地址,然后自身进入堆中,让js执行引擎去堆中找,而堆其实就是js中负责存放引用类型的地方,就像这样:
在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);
它的执行结果是这样的:
而这时我们去看下obj2是什么样的:
我们可以看到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);
这是它的结果:
这么一看,a的值没有改变,而b的n值却改变了,这样是算浅拷贝还是深拷贝呢?当然是浅拷贝,这不是受到了原对象的影响了吗,而深拷贝是不会受到原对象的影响的,讨论完这个问题,又有一个问题,为什么a不受影响而b受影响?
其实它的原理是这样的:
当我们调用这个方法时,它会去obj的引用地址上去把obj的属性拷贝下来,而a就直接被拷贝了,而b也是直接被拷贝了,但b存储的是引用地址,引用地址被拷贝到obj2上,那么obj和obj2的b指向不就是同一个了吗,那么去改obj的b就是在改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;
}
效果如下:
这就是一份手写的浅拷贝,十分的简单,所以面试官不常考。接下来是深拷贝。
深拷贝
不像浅拷贝,目前为止,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);
结果如下:
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);
结果如下:
这就是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]。
效果如下:
深拷贝就结束了。
结语
这是js学习旅程的又一难点,(其实也不是很难),希望这篇文章能对你有所帮助,我是Ace,我们下次再见!!!