纯干货+实操讲解:拿下拷贝只需要读这一篇文章
前言
在今天的软件开发中,数据拷贝是一个既基础又关键的操作。特别是在处理复杂数据结构如对象和数组时,理解并正确实现数据的拷贝对于避免潜在的错误和性能问题至关重要。在JavaScript中,数据的拷贝并非总是直观和简单的,因为它涉及到基本数据类型和复杂数据类型的不同处理方式。
拷贝
拷贝分为基本数据类型拷贝和复杂数据类型拷贝
在JavaScript中,基本数据类型(如数字、字符串、布尔值等)的拷贝是值拷贝,即直接复制变量的值。然而,当涉及到对象和数组等复杂数据类型时,情况就变得复杂了。由于这些数据类型是通过引用传递的,简单的赋值操作并不会创建一个新的对象或数组,而只是复制了一个指向原始数据的引用。这意味着,如果你试图修改这个“拷贝”的数据,实际上你修改的是原始数据,这可能会导致不可预见的副作用。因此,在JavaScript中进行正确的数据拷贝是一项重要的技能。
本文将着重讲解关于复杂数据类型拷贝--浅拷贝和深拷贝
浅拷贝
概念
浅拷贝只会复制对象的引用地址,而不是实际的数据结构。 这意味着,如果修改了原对象或其引用类型属性,通过浅拷贝得到的新对象也会受到影响, 因为浅拷贝复制的实际上是引用类型值的内存地址,它们实际上指向的是同一个数据结构。
常见的浅拷贝方法:
1. Object.create()
let a = {
name: 'Tom'
}
let b = Object.create(a)// 拷贝了一份a
a.name = 'Alan'
console.log(b.name);//Alan
//拷贝出来的对象是a的拷贝,当a改变的时候,b也会改变
我们在讲原型链那一章的时候特别注意过object.create()这个方法。当他生效时他会在原对象的基础上新创建一个对象,新创建的对象将能够访问原型链上的属性和方法,这取决于()中的输入,如果()中输入的是a那么它将继承a对象的属性;但是当()中输入值为null时他不会具有原型。
在这个例子中我们通过Object.create()这个方法对于属性和原型的继承性来进行拷贝,这里提一嘴,为什么他能起到类似复制的效果呢?因为它将 a
对象设置为了新创建对象 b
的原型([[Prototype]]
)。注意,虽然这里连同他的原型一起复制过来了但是他仍然实现了拷贝属性的功能,在传统的拷贝中,我们是不希望连同原对象的原型也一起复制的。
那我们要怎么判断他是深拷贝还是浅拷贝呢?我们直接利用浅拷贝的概念----如果修改了原对象或其引用类型属性,通过浅拷贝得到的新对象也会受到影响。我们只需要更改a中的属性再看b中的属性发不发生变化,如果拷贝出来对象的属性随着原对象的更改而改变那么这种拷贝方式就是浅拷贝。
2. object.assign({},a)
Object.assign({}, a)
是一种实现浅拷贝的方式。这个方法会将 a
对象的所有可枚举自有属性复制到新创建的对象 {}
中。
let a = { name: 'Tom' };
let b = { age: 18 };
let c = Object.assign({}, a, b);
console.log(a, c);
我们创建一个新对象c,通过这个方法可以将a,b复制到这个新对象中,并且不会改变原对象的值不同的是,如果我们不加{}
,那么通过Object.assign(a,b)
会改变原对象a,将b对象加到a中,以下是有{}和无{}的a,c集合打印结果对比:
3. concat
在上一篇《面试重点:数组扁平化有哪些方式(超详全解)》的讲解中,我们了解运用了concat
方法。在上一篇文章我们通过他来合并数组,通过concat的合并,原数组不会改变。 他同样可以进行对象的复制合并,达到拷贝的效果:
let arr = [1, 2, 3,{a: 1}]
let newArr = [].concat(arr)
//将arr中的元素合并到[]中,并返回一个新数组
concat同时能复制多个对象放到新数组,新对象中,非常便捷。
4. 数组解构 ...
同理,通过解构也可以实现浅拷贝:
let arr = [[1, 2], [3, 4]];
let newArr1 = [].concat(arr); // newArr1 是 [[1, 2], [3, 4]]
let newArr2 = [].concat(...arr); // newArr2 是 [1, 2, 3, 4],因为扩展运算符将二维数组展开为一维
-
newArr1
并没有实现arr
中子数组的浅拷贝,而是将arr
本身作为了一个元素进行了拷贝。 -
newArr2
实现了arr
中子数组的浅拷贝,即它包含了arr
中子数组元素的副本。
5. arr.toReversed().reverse()
很新的浅拷贝方法,通过倒序获取再翻转得到原对象
let arr = [1, 2, 3, { a: 1 }]
let res = arr.toReversed().reverse()
console.log(res);
6. arr.slice(0)
slice是数组自带的从数组中提取一部分元素的方法,这个方法能返回一个新的数组对象。我们可以通过他的提取和创建新对象这两性质来达到拷贝的效果。
let arr = [1, 2, 3, { a: 1 }]
let s = arr.slice(1, 3)
//截取数组里的某一段值(左闭右开),原数组不受影响,和splice不同
console.log(s);//[2,3]
let arr2 = [1, 2, 3, { a: 1 }]
let t = arr.slice(0)//从最左边开始截取到最右边,生成新对象
console.log(t);//[1, 2, 3, { a: 1 }]
大家还记得数组中的splice
方法吗?我们来对比一下两种方法
splice
vs slice
1). 功能
-
slice()
:主要用于提取数组的某个部分而不修改原始数组 -
splice()
: 用于修改原始数组的内容
2). 参数
slice(start[, end])
:
start
:开始提取的索引位置(从0开始)。如果是负数,则表示从末尾开始计算。
end
:停止提取的索引位置(不包括该位置)。如果省略,则提取到数组末尾。如果是负数,则表示从末尾开始计算。
splice(index[, deleteCount[, item1[, item2[, ...]]]])
:
index
:开始修改的索引位置。
deleteCount
:要删除的元素数量。如果设置为 0,则不删除元素。
item1, item2, ...
:要插入数组的元素,从 index
位置开始。
3).实操
- 使用
slice()
提取数组的一部分:
let arr = [1, 2, 3, 4, 5];
let newArr = arr.slice(1, 4);
// newArr = [2, 3, 4], arr = [1, 2, 3, 4, 5]
将下标1
到3
的元素(不包括下标4
)提取出来,创建一个新的数组newArr
。
- 使用
splice()
修改数组:
let arr = [1, 2, 3, 4, 5];
let removed = arr.splice(1, 2, 'a', 'b');
// removed = [2, 3], arr = [1, 'a', 'b', 4, 5]
从下标为1的元素开始删除,删除个数为2,在新的下标1前插入a,b
深拷贝
概念
深拷贝会创建新的数据结构,并将原对象的数据复制到新结构中 新对象与原对象是完全独立的,修改新对象不会影响原对象。
常见的深拷贝方法:
1.JSON.parse(JSON.stringify(obj))
他是在js中常见的用于深度复制对象或数组的一种方法
let obj = {
name: 'Tom',
age: 18,
like: {
n: 'coding'
},
a: true,
b: null,
c: undefined,
d: Symbol(),
f: function () { }
}
let obj2 = JSON.parse(stringify(obj))
console.log(obj, obj2);
在上述代码中我们通过一行代码对obj进行了深拷贝:
-
JSON.stringify(obj)
将JS对象或值转换为一个JSON字符串; -
JSON.parse(...)
将JSON字符串转换回一个JS对象或值。
但是这种方法存在3个缺陷:
- 不能识别BigInt类型
- 不能拷贝 undefined、函数、Symbol 类型的
- 不能处理循环引用
2.structuredClone()
很新的方法,前几年刚刚打造,很多浏览器还没跟上,但是谷歌可以实现他的功能。
const obj = {
name: 'John',
age: 30,
hobbies: ['reading', 'coding'],
date: new Date(),
regex: /abc/g,
func: function() { console.log('Hello!'); }
};
const clonedObj = structuredClone(obj);
console.log(clonedObj);
// 输出与原始对象相同的内容,但实际上是新的对象实例;
structuredClone()
支持复制函数、循环引用、日期对象等等JSON.stringify()
无法处理的属性
。在上面的示例中,clonedObj
是 obj
的深拷贝,包括嵌套的对象、日期对象、正则表达式和函数等都被正确地复制了。同时,由于 structuredClone()
保持了原始类型,所以复制后的日期对象、正则表达式和函数仍然是相应的类型,而不是字符串或其他类型。
总结
在本篇文章中我们学习了基本数据类型的拷贝和复杂数据类型拷贝,了解了值拷贝,深度学习了浅拷贝和深拷贝;本文目标希望读者能够熟悉并且掌握浅拷贝的6种方法和深拷贝的2种方法,透析深浅拷贝的基本原理。再后续的学习中我们将利用深浅拷贝的原理手搓一个新函数,对拷贝的理解更上一个阶梯。
转载自:https://juejin.cn/post/7371445601555087411