JS深浅拷贝

293 阅读5分钟

在前端开发中我们经常会遇到拷贝的问题,这也是一个面试经常会被问到的问题。 我们都知道有深拷贝和浅拷贝,那我们来聊聊它们的形成原因和常见的深浅拷贝方法

一、深浅拷贝的原因

  • JS数据类型分为
8种数据类型:'Number''String''Boolean''Null''Undefined''Object''Symbol''BigInt'

基本类型:StringNumberBooleanNullUndefinedSymbolBigInt

引用类型:Object

  • JS数据存储
不同类型数据存在不同的内存中

栈内存:存放基本数据类型引用数据类型的地址(也可理解为指针)

堆内存:存放引用数据类型的值也就是对象

  • 深浅拷贝
深浅拷贝一般都是相对于'引用数据类型'来进行探讨的;
当然对于'基本数据类型'来说,拷贝也都可理解为深拷贝(完全复制)

浅拷贝:将原来栈内存中存储的地址(指针)进行了一份复制存到新的栈内存地址,所指向堆内存数据还是原来的数据

深拷贝:将原来的内存的数据(栈内存和堆内存)进行了一份复制存到新的地址中,新的栈内存中的新地址指向的是新的堆内存中的数据

二、深浅拷贝的方法

1、浅拷贝方法

浅拷贝一般就是栈内存数据copy一份存到新的栈内存中,比如:

对于普通类型:

let a=1;
let b=a;
console.log(a,b);// 1,1

b=2;
console.log(a,b);// 1,2  
// 可以看到b的改变,并未影响a的值,即两个是独立的两个栈内存,互不影响

// 另外注意一点,基本数据类型不能增加新的属性,
b.c=3;
console.log(b) // undefined

对于引用类型:

let obj={a:1,b:2};
let objCopy = obj;
console.log(obj,objCopy); //{a: 1, b: 2} {a: 1, b: 2}

objCopy.b=3;
objCopy.c=4;
console.log(obj,objCopy); //{a: 1, b: 3, c: 4} {a: 1, b: 3, c: 4}

// 可以看到对于引用类型的数据,copy以后的改动还是对原堆内存中数据的修改

2、深拷贝方法

展开运算符...

Object.assign

let obj = { a: 1, b: 2};
let objCopy = Object.assign({},obj)
let objCopy2 = {...obj}

console.log('obj:', obj)  //{a: 1, b: 2}
console.log('objCopy:', objCopy) //{a: 1, b: 2}
console.log('objCopy2:', objCopy2) //{a: 1, b: 2}

objCopy.c = 4
objCopy2.c = 5

console.log('obj:', obj) //{a: 1, b: 2}
console.log('objCopy:', objCopy) //{a: 1, b: 2,c: 4}
console.log('objCopy2:', objCopy2) //{a: 1, b: 2,c: 5}

以上代码看起来貌似没什么问题,然而,当obj的key对应的value是Object类型时,就会出现问题。

//以 Object.assign 为例,展开运算符是一样的结果
let obj = { a: 1, b: 2, c: { c1: 0 } };
let objCopy = Object.assign({},obj);  

console.log('obj:', obj) //{a: 1, b: 2, c: {c1: 0}}
console.log('objCopy:', objCopy) //{a: 1, b: 2, c: {c1: 0}}

objCopy.c.c1 = 4

console.log('obj:', obj) //{a: 1, b: 2, c: {c1: 4}}
console.log('objCopy:', objCopy) //{a: 1, b: 2, c: {c1: 4}}

// 可以看出当key对应的value为Object时,改动objCopy 仍然会影响 obj

那么这两种拷贝方法就只能局限在对象的key对应的value 只能是原始类型

JSON.parse与JSON.stringify

通过JSON.stringify(Object) 转换为String类型,进行基本类型的拷贝; 再通过JSON.parse() 将经过转换的特殊String再转为Object。

这是我们经常用的深拷贝的方法,举例:

let obj = { a: 1, b: 2, c: { c1: 3 } };
let objCopy = JSON.parse(JSON.stringify(obj))

console.log('obj:', obj) //{ a: 1, b: 2, c: { c1: 3 } }
console.log('objCopy:', objCopy) //{ a: 1, b: 2, c: { c1: 3 } }

objCopy.c.c1 = 4

console.log('obj:', obj) //{ a: 1, b: 2, c: { c1: 3 } }
console.log('objCopy:', objCopy) //{ a: 1, b: 2, c: { c1: 4 } }

//可以解决展开运算符...与Object.assign()的缺陷,对多层嵌套也可以很好的拷贝

然而,当obj的key对应的value是Object某些特殊类型时,如Date对象RegExp对象Function函数时,会发现这种方法并不能完美的copy

1. key对应的value为 Function
let obj = { a: 1, b: 2, c: function () { console.log(111) } };
let objCopy = JSON.parse(JSON.stringify(obj))
//输出objCopy为 {a: 1, b: 2}  objCopy丢弃了c:function(){}

2. key对应的value为 RegExp
let obj = { a: 1, b: 2, c:RegExp("123") };
let objCopy = JSON.parse(JSON.stringify(obj))

//输出objCopy为 {a: 1, b: 2, c: {}}   objCopy.c的value被转换为{}

3. 
let obj = { a: 1, b: 2, c:new Date() };
let objCopy = JSON.parse(JSON.stringify(obj))

//输出objCopy为 {a: 1, b: 2, c: "2020-07-06T11:21:11.234Z"} objCopy.c的value被转换为 日期字符串

4. 还有其他的类型值,可自行实验一下
参考`stackoverflow`:
you do not use Dates, functions, undefined, Infinity, RegExps, Maps, Sets, 
Blobs, FileLists, ImageDatas, sparse Arrays,Typed Arrays or other 
complex types within your object,
a very simple one liner to deep clone an object is.


5. 当对obj循环引用时
let obj = { a: 1, b: 2 };
obj.c=obj;
let objCopy = JSON.parse(JSON.stringify(obj))

报错:Uncaught TypeError: Converting circular structure to JSON
      --> starting at object with constructor 'Object'
      --- property 'c' closes the circle
      at JSON.stringify (<anonymous>)
受限于JSON.stringify() 可转换的数据必须是非infinite层级结构的数据

通过以上的例子可以明显看出,JSON转换的形式仅适用于符合json数据格式的数据结构;其限制也比较多,但相比于前几种形式优点也不言而喻,只要能确定数据符合要求,即可放心适用这种方法

递归

递归方法众多,可自行实现;这里不再实现。
考虑一个问题:如果是"循环引用的对象",必然会造成死循环。
怎么能解决这个问题?(thinking...)

已经存在的轮子,loadsh的cloneDeep()

如何使用可以查看Loadsh官网