背景
- 在JavaScript中,数据结构可以分为基本数据类型(如数字、字符串、布尔值等)和引用数据类型(如对象、数组、类实例等)。基本数据类型存储的是实际的值,而引用数据类型存储的是数据在内存中的地址。因此当我们需要复制一份数据时,如果简单地复制引用类型的地址而不复制其内容,可以节省内存和提高效率,但这可能导致原数据和副本之间的意外关联,即修改一个会影响另一个,这在某些情况下是不可接受的。
- 而我们讨论的拷贝通常只讨论在引用类型上,,因为基本数据类型存储的是直接的值,而不是引用。当你将基本数据类型的变量赋值给另一个变量时,实际上是创建了一个新的存储空间来存放这个值的一个副本。这意味着改变其中一个变量的值不会影响到另一个变量。因此,对于基本数据类型,拷贝行为总是“深拷贝”性质的,无需特别区分深浅拷贝。
浅拷贝
浅拷贝其实就是一句话: “拷贝的是数据的地址”
怎么理解这句话:浅拷贝过程实质上是创建了一个新的变量,但这个新变量与原变量指向同一个内存地址上的对象。这意味着原对象和拷贝对象共享相同的数据结构和内部状态。因此,对拷贝对象所做的任何修改,如果涉及到修改共享的数据结构,也会影响到原始对象。同样的,原始对象所做的任何修改,如果涉及到修改共享的数据结构,也会影响到拷贝对象。
浅拷贝的对象{}方法
1. Object.create()
代码语言:javascript复制ini
复制代码
let obj={
a:1
}
let obj2=Object.create(obj)
Object.create()
通过原型链的方法,将原始对象设置拷贝对象隐式原型,这个方法并不优雅,虽然能访问到数据。但是我们可以通过实验证明它的问题:
css
复制代码
let obj={
a:1
}
let obj2=Object.create(obj)
obj2.a=100
console.log(obj)//{ a: 1 }
console.log(obj2)//{ a: 100 }
从 obj2.a=100
这一步开始,并不是修改了 obj
的 a
,而是给 obj2
新增了一个自身的 a
属性,覆盖了从原型链上继承来的 a
的值。因此,输出结果表明 obj
和 obj2
的 a
属性值不同,但这并不是通过 Object.create()
直接实现的浅拷贝效果,而是因为你在 obj2
上新定义了一个同名属性,覆盖了继承来的属性。
2. Object.assign()
Object.assign()
方法的主要作用是将一个或多个源对象的所有可枚举自有属性的值复制到目标对象中。
ini
复制代码
let obj1 = { a: 1, b: { c: 2 } };
let obj2 = { b: 3, d: 4 };
let newObj = Object.assign({}, obj1, obj2);
console.log(newObj); // { a: 1, b: 3, d: 4 }
浅拷贝的数组[]方法
1. [].concat()
[].concat()用于将一个或多个数组(或非数组值)连接到原数组的副本,并返回连接后的新数组。这个方法不会改变原数组,而是返回一个包含连接结果的新数组。
代码语言:javascript复制ini
复制代码
let arr=[1,2,3,{a:1}]
let arr2=[].concat(arr)
console.log(arr2)//[1,2,3,{a:1}]
这里虽然是出创建的新的地址,但是内部{a:1}中仍然存放的是a数据引用地址
代码语言:javascript复制ini
复制代码
let arr=[1,2,3,{a:1}]
let arr2=[].concat(arr)
arr[3].a=2
console.log(arr2)//[1,2,3,{a:2}]
2. 解构(...arr)
结构方法就不多叙述了,放个例子看吧,过程与[].concat()是相似的
代码语言:javascript复制ini
复制代码
let arr=[1,2,3,{a:1}]
let arr2=[]
arr2.push(...arr)
console.log(arr2)//[1,2,3,{a:1}]
3. arr.slice(0,arr.length)
数组中的slice()
方法也不过多赘述,记住内部的区间是左闭右开就行了。
ini
复制代码
let arr=[1,2,3,{a:1}]
let arr2=arr.slice(0,4)
console.log(arr2)//[1,2,3,{a:1}]
4. arr.toReversed().reverse()
大家都知道reverse()
是用于反转数组的操作,而在数组的原型链上有一个方法toReversed()
,它的作用是,不改变原数组,返回一个原数组的反转数组。因此我们可以利用这个返回的数组进行二次反转,然后便得到一个浅拷贝数组。
ini
复制代码
let arr=[1,2,3,{a:1}]
let arr2=arr.toReversed().reverse()
console.log(arr2)//[1,2,3,{a:1}]
手写通用shallowCopy函数
代码语言:javascript复制scss
复制代码
function shallowCopy(obj) {
//判断是数组还是对象
let obj2 = Array.isArray(obj) ? [] : {};
for (let key in obj) {
//判断key 是不是obj 显示具有的
if (obj.hasOwnProperty(key))
obj2[key] = obj[key]
}
return obj2
}
上面注释的这两步是非常重要的,判断是数组还是对象
如果直接创建let obj2={}
那么当你传入一个数组arr=[ 1, 2, 3 ]
拷贝时结果会是:{ '0': 1, '1': 2, '2': 3 }
判断key 是不是obj 显示具有的
如果当原型链上挂载着其他数据时,for of循环
会将挂载在原型链上的数据也拷贝下来,这显然是不合理的。
css
复制代码
Object.prototype.c = 1
let obj = {
a: 1,
b: { n: 2 }
}
let obj2 = {};
for (let key in obj) {
obj2[key] = obj[key]
}
console.log(obj2);//{ a: 1, b: { n: 2 }, c: 1 }
深拷贝
深拷贝的常用方法
1. JSON.parse(JSON.stringify())
代码语言:javascript复制css
复制代码
let obj={
a:1,
b:{n:2},
c:'cc',
}
let obj2=JSON.parse(JSON.stringify(obj))
obj2.b.n=10
console.log(obj);//{ a: 1, b: { n: 2 }, c: 'cc' }
console.log(obj2);//{ a: 1, b: { n: 10 }, c: 'cc' }
arr=[1,2,3,{n:1}]
let arr2=JSON.parse(JSON.stringify(arr))
arr[1]=20,arr[3].n=100
console.log(arr);//[ 1, 20, 3, { n: 100 } ]
console.log(arr2);//[ 1, 2, 3, { n: 1 } ]
老程序员这个方法肯定门清的很,转为JSON字符串后再转回就会得到一个新的数据,这段数据是新开辟的内存,不再与原来数据有关系。JSON.parse(JSON.stringify())
方法也是有几个缺点的。
- 无法识别bigInt类型
function
、undefined
、Symbol
类型丢失无法被拷贝:原始对象内部有这集中数据类型时,拷贝对象中这些数据将会直接被丢失。- 原型链信息丢失:拷贝后的新对象不会保留原对象的原型链信息,这意味着通过原型继承的属性和方法在拷贝对象上将不可用。
- 性能开销:这种方法涉及到了两次转换(先序列化为JSON字符串,再反序列化为对象),这在处理大型对象或深层嵌套结构时可能会带来较大的性能开销。
- Date、RegExp、Error等特殊对象转换:这些对象在经过
JSON.stringify()
序列化后会丢失它们的原始类型信息,变成普通的对象或字符串,通过JSON.parse()
反序列化回来时,它们不再是原来的类型。 - 无法处理循环引用:当试图序列化一个包含循环引用(即对象A的某个属性引用了对象B,而对象B的某个属性又直接或间接引用了对象A)的对象时,
JSON.stringify()
会抛出错误,因为它无法正确处理这种结构,从而导致整个操作失败。
2. structureClone()
structuredClone()
是比较新的一种深拷贝方法,当使用structuredClone()
时,注意检查当前运行环境对该方法的支持情况,因为它在一些较旧或不遵循最新标准的浏览器中可能不可用。对于不支持的环境,可能需要回退到其他深拷贝实现。
ini
复制代码
let obj={
a:1,
b:{n:2},
e:null,
f:undefined
}
const newObj= structuredClone(obj)
obj.b.n=3
console.log(obj);//{ a: 1, b: { n: 3 }, e: null, f: undefined }
console.log(newObj);//{ a: 1, b: { n: 2 }, e: null, f: undefined }
let arr=[1,2,{m:3}]
let newArr=structuredClone(arr)
arr[0]=10,arr[2].m=6
console.log(arr);//[ 10, 2, { m: 6 } ]
console.log(newArr);//[ 1, 2, { m: 3 } ]
可以看到,structuredClone()
对于undefined
数据是可以被正常拷贝的,但是对于function
、Symbol
类型也是丢失无法被拷贝。
手写通用deepCopy函数
代码语言:javascript复制javascript
复制代码
function deepCopy(obj, hash = new WeakMap()) {
if (obj === null) return null;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
// 处理循环引用
if (hash.has(obj)) {
return hash.get(obj);
}
// 定义obj2应在处理循环引用之后,避免提前引用未定义变量
let obj2 = Array.isArray(obj) ? [] : {};
hash.set(obj, obj2); // 此处应确保obj2已被定义
for (let key in obj) {
if (!obj.hasOwnProperty(key)) continue; // 忽略原型链上的属性
if (typeof obj[key] === 'object' && obj[key] !== null) {
obj2[key] = deepCopy(obj[key], hash); // 使用递归并传递hash
} else {
obj2[key] = obj[key];
}
}
return obj2;
}
重点优点:
- 处理特殊类型:对
Date
和RegExp
这样的特殊类型进行了特别处理,确保拷贝的是新实例而非原始对象的引用。 - 循环引用处理:使用
WeakMap
来存储已经拷贝过的对象引用,以此来解决循环引用的问题。这样当遇到已经拷贝过的对象时,直接从WeakMap
中返回其拷贝,避免无限递归。 - 排除原型链属性:通过
hasOwnProperty
确保只拷贝对象自身的属性,而不包括继承自原型链的属性。
这样做的deepCopy
函数更加健壮,能更好地处理各种复杂对象结构的深拷贝需求。