当谈及拷贝 ,你的第一印象会不会和我一样,ctrl c + ctrl v ... ;虽然效果和拷贝是一样的,但是你知道拷贝的原理以及它的实现方法吗?今天就让我们一起探究一下拷贝中深藏的知识点吧。
拷贝
首先来看下面一段代码;
ini
let obj = {
age: 18
}
let obj2 = obj
obj.age = 20
console.log(obj2.age);
上面的let obj2 = obj
这一操作能叫拷贝吗?并不能 ;因为这个操作只是让obj2
只是获得了 obj
的引用地址 ,这意味着 obj2
和 obj
指向内存中的同一个对象 ,而obj2
并未创建一个和obj
一样的新对象 ;当对象obj
身上的age
改变时,因为二者的引用地址一样 ,obj2.age
也会跟着变化。
所以,在JavaScript中,"拷贝" (Copy)是指创建一个对象或数组(引用类型)的副本 。根据拷贝的层次,可以分为两种类型:浅拷贝(Shallow Copy)和深拷贝(Deep Copy)。接下来我们就来细聊它们的区别以及实现的方式:
浅拷贝
浅拷贝 (Shallow Copy),它创建一个新对象作为原对象的副本;但是在创建的新对象上,只有原始类型 (如Number,String,Boolean等)的值会和原对象上的保持相同 ,而新对象上的引用类型会随着原对象的改变而改变 ,因为浅拷贝并不会递归地复制这些引用类型内部的元素,而是将引用的地址复制过去 ,这就导致新对象和原对象虽然在顶层看似独立,但是共享了内部引用类型的数据。
所以说,浅拷贝后的新对象会受到原对象的影响;那在js中有哪些方法可以实现浅拷贝呢?
1. Object.create(obj)
在JavaScript 中,Object.create()
用于创建一个新对象 ,同时指定新创建的对象的原型 (即其内部的 [[Prototype]]
隐式原型属性)。
ini
let obj = {
a : 1
}
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
上的属性和方法。
2. Object.assign({},obj)
在JavaScript中,assign()
方法是 Object
构造函数的一个静态方法,用于将一个或多个源对象的可枚举属性的值复制到目标对象中。此方法会在目标对象上进行就地修改,并返回修改后的目标对象。
css
let obj = {
a: 1,
b: [1,2]
}
let obj2 = Object.assign({},obj) // Object.assign(a,b) 将对象b合并到a中
obj.b.push(3)
console.log(obj2.b);
利用这一特性,我们可以通过Object.assign({},obj)
来完成浅拷贝 的操作;将目标对象里的属性都复制到一个空对象中 ,但请注意,如果 obj
中的属性是引用类型 (如数组、对象),只会复制引用类型在堆中的地址,这些属性在新对象中仍然是通过引用共享的,且会被原对象影响。
3. [].concat(arr)
[].concat(arr)
是 JavaScript 中用于将数组 arr
连接到一个新数组中 的方法。它会返回一个新数组 ,其中包含原始数组和 arr
中的所有元素。利用这个方法,我们可以完成一个数组的拷贝。
ini
let arr = [1,2,3,{a:1}]
let arr2 = [].concat(arr)
arr[3].a = 2
console.log(arr2);
同样的,当要拷贝的数组中存有引用类型 的值(如对象)时,拷贝过程中也只能复制其引用地址 ,引用类型在新数组中同样也是共享原数组的。
4. 数组解构 ...
在数组中,也可以通过解构 的方法,实现浅拷贝;在js中解构数组用...
表示。
ini
let arr = [1,2,3,{a:1}]
let arr2 = [...arr] // 将原数组元素解构到新的空数组当中
arr[3].a = 2
console.log(arr2);
与使用 concat
方法一样,使用...
扩展运算符进行浅拷贝也会导致新数组中的对象或数组元素仍然是原始数组中相同对象或数组的引用。
5. arr.slice(0)
在js中,数组身上的slice
方法,是用于从数组中提取一个新的子数组的方法 。它接受两个参数,起始索引和结束索引 (可选),并返回一个包含从起始索引到结束索引(不包括结束索引)的元素的新数组。如果省略结束索引,则提取从起始索引到数组末尾的所有元素。利用这一方法,也可以实现数组的浅拷贝:
ini
let arr = [1,2,3,{a:1}]
let arr2 = arr.slice(0)
arr[3].a = 2
console.log(arr2);
6. arr.toReversed().reverse()
在js中,我们还可以利用"两极反转"的特性实现数组浅拷贝 的功能。数组中,有一个方法叫做,toReversed()
,它可以把原数组给反转,但是它并没有返回值 ;而数组中还有一个方法叫做,reverse()
,它的作用和toReversed()
是一样的,也是反转数组,但是它会返回反转后的数组 。利用这个特点,我们就可以将这两种方法结合,两极反转一下,实现浅拷贝:
ini
let arr = [1,2,3,{a:1}]
let arr2 = arr.toReversed().reverse()
arr[3].a = 2
console.log(arr2);
手写方法实现浅拷贝
浅拷贝的原理,即是创建一个副本对象,并把原对象的原始类型值复制过来,而引用类型则是复制其引用地址。在知道原理后,手动写一个实现浅拷贝的方法也很简单:
vbnet
function shallowCopy(obj){
let newObj = {}
for(let key in obj){
// key 是不是obj显示具有的
if (obj.hasOwnProperty(key)){
newObj[key] = obj[key]
}
}
return newObj;
}
let obj = {
a: 1,
b: {n:2}
}
console.log(shallowCopy(obj));
在上面所示的方法中,是利用 for...in
循环拿到要拷贝对象中的属性;由于,for...in
循环在JavaScript 中,可以遍历对象的可枚举属性,包括自身的属性和继承的属性 。这意味着在遍历过程中,可能会访问到对象的隐式属性,例如原型链上的属性 。通常情况下,我们是不需要拷贝对象原型上的属性的 ,所以我们可以结合 hasOwnProperty
方法进行判断,规避原对象隐式具有的属性 ,以确保只遍历对象自身的属性。
深拷贝
知道了浅拷贝之后,深拷贝也很好理解;深拷贝 指的是将一个对象完整地复制到一个新的对象中,包括其所有属性和嵌套对象的属性,而不是仅仅复制其引用。这样做可以确保新对象与原始对象完全独立,修改新对象不会影响原始对象。
深拷贝的对象是不会受原对象的影响的。在js中有以下方法可以实现深拷贝:
1. JSON.parse(JSON.stringify(obj))
在js中,JSON.parse(JSON.stringify(obj))
是一种常用的深拷贝对象的方法。它利用了 JSON 对象的序列化和反序列化功能,通过将对象转换为 JSON 字符串,再将其解析为新的对象,从而实现深拷贝。
ini
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); // 输出 1
console.log(obj.b.c); // 输出 2
可以看到,当修改 newObj
的属性时,原始对象 obj
的属性并没有受到影响,这表明深拷贝成功实现了对象的完全独立复制。
但是JSON.parse(JSON.stringify(obj))
方法存在一些局限性:
- 无法识别bigInt类型
- 无法拷贝 undefined,function,Symbol
- 无法处理循环引用
在大多数情况下,JSON.parse(JSON.stringify(obj))
是一个简单且有效的深拷贝方法,但在处理特殊类型的属性或循环引用时,可能需要考虑其他深拷贝的实现方式。
2. structuredClone()
structuredClone()
是一个较新的JavaScript API,它提供了一种创建对象、数组以及一些特殊类型值的深拷贝的方法。
yaml
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 = 20
console.log(newObj);
在上面的执行结果中,可以看到structuredClone()
方法,不仅能实现深拷贝 ,而且还能够识别bigInt
类型的值,以及undefined
;并且structuredClone()
API 有一个显著的优点,即它能够妥善处理对象的循环引用。
手写方法实现深拷贝
在我们要手写一个深拷贝的方法 时,你脑子里会用什么思想去实现它?没错,就是递归 ;因为深拷贝就是要把对象中的引用类型(对象等)里面的原始值给复制过来,遇到引用类型,就要获取里面的原始值,直至没有引用类型了。
ini
let obj = {
a: 1,
b: {n: 2}
}
function deepCopy(obj){
let newObj = {};
for(let key in obj){
if(obj.hasOwnProperty(key)){
// obj[key] 是不是对象 typeof(obj[key]) == 'object' &&
if (obj[key] instanceof Object){
newObj[key]=deepCopy(obj[key])
}else {
newObj[key] = obj[key];
}
}
}
return newObj;
}
let obj2 = deepCopy(obj)
obj.b.n = 20
console.log(obj2);
在上面的代码中,obj[key] instanceof Object
这段代码用于检查 obj
对象中键为 key
的属性值是否为对象,这意味着它会判断该属性值是否是一个对象 (普通对象、数组、函数等,但不包括null,因为null没有原型);如果是对象类型的话,则递归调用;直到obj[key]
不是一个对象类型时,就会停止递归调用。
总结
浅拷贝 适用于对象结构简单且不需要复制嵌套对象 的情况,而深拷贝 则适用于对象结构复杂,需要完全独立复制所有层级属性 的场景。选择哪种拷贝方式取决于具体需求,但需注意深拷贝因为需要递归处理,所以在性能上相对较低效。