前言
在前端开发中处理对象和数组时,我们会经常碰到数据拷贝的场景,如果出现一些不恰当的拷贝就会导致一些难以预料的错误发生,难以排查与维护。拷贝又常常分为深拷贝和浅拷贝,要想成为一名优秀的程序员,弄清楚两者的概念及区别尤为重要。
这里举个错误使用拷贝的简单例子:
我相信大家对于这种表格并不陌生,在表格的操作列中有一个显示详细数据的操作,这里需要实现一个数据回显的功能。
如果这里只是简单的通过回显数据 = 表格数据 直接赋值就会出现一个问题,当我在回显数据上使用了修改功能后,哪怕没有选择确定或者保存,外面表格的数据都会相应的发生变化。
这种就是典型的浅拷贝带来的问题,对拷贝数组做出的更改有时会影响到源数组。注意这里的有时,后文会介绍为什么。
引言
const a = 10;
cosnt b = a;
console.log(b);
毫无疑问,输出结果是10。
为了深入理解拷贝这一过程,我们需要研究数据在内存中的存储方式。
- 再看个例子:
const arr = [0,1,2]
const arr_copy = arr
-
我们再看一种情况:
大家思考一下输出结果是什么
const a = [1,2,[3,4]];
const b = [...arr];
b[0] = 2;
b[2] = 1;
console.log(a);
console.log(b);
答案是:[1,2,[3,4]] 和 [2,2,1]
那如果我修改一下呢
const a = [1,2,[3,4]];
const b = [...arr];
b[0] = 2;
b[2][0] = 1;
console.log(a);
console.log(b);
答案是: [1,2,[1,4]] 和 [2,2,[1,4]]
相信真正理解了深浅拷贝的同学都能做对,但如果有做错的说明你还没有真正的理解深浅拷贝。
当我们将a使用展开运算符或者其它标准的内置对象复制操作方法拷贝给b时,会开辟一个新的内存空间用于存放b的数据。但是对于a存放的数据,它会原封不动的复制过去,包括a存放的引用,这也就导致了b中的引用数据类型属性会与a共享引用。
我们看一下图解:
那这时候有同学就有疑问了,我明明将b[0]修改了a[0]却没有变,为啥这个还是浅拷贝呢?不是说浅拷贝之后源对象和拷贝对象更改会互相影响吗?这就是我上文提到的钱拷贝的概念,对拷贝数组做出的更改有时会影响到源数组。
一、深浅拷贝的定义
现在我们引出浅拷贝的定义,这里参考的是MDN上的定义。
浅拷贝
对象的浅拷贝是其属性与拷贝源对象的属性共享相同引用(指向相同的底层值)的副本。
注意这里所说的是共享相同引用,对于不是引用类型的数据,自然不会影响到浅拷贝的判定了。
实际上,在 JavaScript 中,所有标准的内置对象复制操作(、、、、 和 )创建的都是浅拷贝而不是深拷贝。
各位可以自己实践一遍,这里就不再赘述了。
深拷贝
那么如何才算是深拷贝呢?继续使用上面提到的数据例子,如果我们实现了源对象和拷贝对象完全,就实现了深拷贝。
这里引出MDN上关于深拷贝的定义如下:
对象的深拷贝是指其属性与其拷贝的源对象的属性不共享相同的引用(指向相同的底层值)的副本。
实际上,MDN上的这句话在我看来是有一些歧义的,比如说源对象和拷贝对象的第一层的属性不共享相同引用,但是第一层属性的嵌套属性却共享引用,这种情况实际上并不属于我们常说的深拷贝,它们并没有完全。
深拷贝应确保整个对象及其嵌套属性都不共享相同的引用,而不仅仅是第一层属性。至于这句话是否有歧义,欢迎各位小伙伴在评论区留下自己的看法,我们共同探讨。
二、如何实现深拷贝
1. 使用JSON序列化
const a = [1,2,[3,4]];
const b = JSON.parse(JSON.stringify(a));
这是最简单有效的方法,但是这种方法存在弊端,想要使用这种方法的前提是数据允许被序列化。
于是对于一些无法序列化的数据自然不能采用这种方法
例如:、、正则表达式、在 中表示 HTML 元素的对象、递归数据以及许多其他情况。
const obj = {
a:{
foo:'bar'
},
}
obj.b = obj
cosnt obj_stringify = JSON.stringify(obj)
const obj_parse = JSON.parse(obj_stringify)
console.log(obj_parse)
function say() {
console.log("圣诞快乐!");
}
const reg = new RegExp()
console.log(JSON.stringify(say));
console.log(JSON.stringify(reg));
2. 使用方法
该方法与JSON序列化类似,但常用于操作定型数组,它们有以下几个不同点:
- 复杂数据类型支持:
structuredClone() 支持复制包括 DOM 节点、Blob、File、ArrayBuffer 等复杂数据类型,而 JSON 序列化只支持基本的 JavaScript 数据类型。 - 用途不同:
structuredClone() 主要用于在 Web Workers、postMessage() 等场景中复制包含复杂数据的对象,而 JSON 序列化主要用于简单数据的转换和传输。 - 兼容性:
structuredClone() 可能不适用于所有 JavaScript 环境,而 JSON 序列化是一种通用且普遍支持的数据序列化方式。
3. 使用外部库,如的_.cloneDeep(value)方法
npm i --save ladash
import _ from 'lodash'
var objects = [{ 'a': 1 }, { 'b': 2 }];
var deep = _.cloneDeep(objects);
console.log(deep[0] === objects[0]);
4. 自己手写一个
const myDeepClone = function (resource) {
if (typeof resource !== 'object' || resource === null) {
return resource
}
const copy = Array.isArray(resource) ? [] : {}
for (const key in resource) {
if (resource.hasOwnProperty(key)) {
copy[key] = myDeepClone(resource[key])
}
}
return copy
}
let symbol = Symbol()
const obj = {
name: '张三',
age: 18,
hobbies: ['篮球', '足球', '乒乓球',{foo:'bar'}],
fun: ()=>{
console.log('hello')
},
[symbol]:'bar'
}
const copy = myDeepClone(obj)
copy.fun()
console.log(copy.hobbies !== obj.hobbies)
console.log(copy.hobbies[3] !== obj)
console.log(copy[symbol]);
这个实现了数据嵌套的深拷贝,以及对函数的拷贝,但是并不能解决循环引用(会发生栈溢出)的问题
实现对循环引用的拷贝:
const myDeepClone = function (resource) {
const cache = new WeakMap()
const _deepClone = function (resource) {
if (typeof resource !== 'object' || resource === null) {
return resource
}
if(cache.has(resource)) {
return cache.get(resource)
}
const copy = Array.isArray(resource) ? [] : {}
cache.set(resource, copy)
for (const key in resource) {
if (resource.hasOwnProperty(key)) {
copy[key] = _deepClone(resource[key])
}
}
return copy
}
return _deepClone(resource)
}
let symbol = Symbol()
const obj = {
name: '张三',
age: 18,
hobbies: ['篮球', '足球', '乒乓球',{foo:'bar'}],
fun: ()=>{
console.log('hello')
},
[symbol]:'bar'
}
obj.itself = obj
obj.hobbies.push(obj)
const copy = myDeepClone(obj)
copy.fun = function(){
console.log('world');
}
obj.fun()
copy.fun()
console.log(copy.fun === obj.fun)
console.log(copy.hobbies !== obj.hobbies)
console.log(copy.itself !== obj.itself)
console.log(copy.hobbies[3] !== obj)
console.log(copy.hobbies[3] === copy)
console.log(copy[symbol]);
改良之后的深拷贝能够实现循环引用的拷贝,但是对于symbol等还是不支持,除此以外还需要考虑原型上的内容是否实现深拷贝,还有对Map、Set、定型数组等等的考虑,所以最好的办法还是根据自己的需求选择最合适的方式,这里锻炼的是一个递归的思想。
参考文献