你不知道的 JS 系列 - 理解 JS 中 赋值,浅拷贝,深拷贝
开篇
又回到了这个老生常谈,新生绝望的问题上,通常遇到这种大家都比较熟悉的问题,反而不知道怎么列大纲,怕不够深入也怕脱离主题~
emm..
此文系 不要再问我 XX 系列之 不要再问我 JS Clone 的问题了
为什么会存在这三种情况?三者有何差异
clone
本来很简单,只是因为 JS 中不同的数据类型存储方式 (堆和栈) 的差异,我们才会觉得它貌似有点‘复杂’
基本类型和引用类型的差异如上图所示了
它们共同的目标就是以一个对象为原型 clone 出另外一个新对象,因为自身的问题产生一些副作用,三者的差异其实就体现在副作用的差异上
差异(堆和栈)
- 栈(stack)为自动分配的内存空间,它由系统自动释放
- 而堆(heap)则是动态分配的内存,大小不定也不会自动释放
基础类型: 值存放在栈中,比较是值的比较
引用类型: 值存放在堆中,变量实际上是一个存放在栈内存的指针,这个指针指向堆内存中的地址。每个空间大小不一样,要根据情况开进行特定的分配,引用类型的比较是引用的比较
1 | var person1 = [1,2,3]; |
赋值
赋值的概念 即使刚入行也不陌生,每天都在用的'='
原理
- 基本类型:在内存中新开辟一段栈内存,然后再把再将值赋值到新的栈中,是两个独立相互不影响的变量
- 引用类型:赋值是传址,是对象保存在栈中的地址的赋值,这样的话两个变量就指向堆内存的同一个对象,因此两者之间操作互相有影响
Demo
1 | var obj1 = { |
理解浅拷贝
之前的很多年,我认为赋值差不多等于浅拷贝
写个小 demo 发现它们之间的差异
1 | var obj2 = obj1; |
赋值对象,是将对象指针直接赋值给另一个变量
浅拷贝,是重新创建了新对象,所以你更改 obj1.name
的时候不会影响到它,但是改变引用类型时就不能幸免了
所谓的浅拷贝就是:
- 当对简单的数据类型进行赋值的时候,其实就是直接在栈中新开辟一个地方专门来存储一样的值
- 当对引用类型进行浅拷贝,后面的对象和前面的对象在第一层数据结构中指向同一个堆地址,但是如果前面的数据不止有一层(属性值是一个指向对象的引用只拷贝那个引用值),类似
1 | language : [1,[2,3],[4,5],[9,0]] |
内部的子对象的指针还是同一个地址
如果要实现一直往下复制 就引出了接下来要说的深拷贝
结论:浅复制要比复制来的深刻一点,至少它开辟了一个新对象,一块儿新的堆内存
目前可行的实现方式
站在巨人的肩膀上,我们可以轻松实现浅拷贝
- 数组的浅拷贝
1 | 1. b = [...a] |
- 对象的浅拷贝
1 | 1. b = Object.assign({},a) |
如果要你自己实现呢
原理:遍历对象的每个属性进行逐个拷贝
1 | function copy(obj) { |
理解深拷贝
深拷贝的意义,就是完全复制,如果你读了上文,应该就没有什么疑问了
将 a 对象复制一份给对象 b,不管 a 中的数据结构嵌套有多深,当改变 a 对象中的任意深度的某个值后,b 中的该值不会受任何影响
目前可行的实现方式
JSON.stringify()``和JSON.parse()
的混合配对使用
1 | var obj4 = JSON.parse(JSON.stringify(obj1)) |
obj1
,obj4
是两个独立的对象,更改数据互不影响,达到了我们要的目的
它粗暴,有用,但是也有缺点
在JSON.stringify()
做序列化时,undefined
、function
以及symbol
值,会被忽略
例如
1 | var obj = { |
结果
如果要你自己实现呢
原理:使用递归,遍历每一个对象属性进行拷贝
1 | var obj = { |
总结
- 赋值:引用复制 执向同一个对象
- 浅拷贝 :生成一个新对象,只能拷贝一层,当属性值是一个指向对象的引用只拷贝那个引用值
- 深拷贝:完全拷贝,前后对象没有任何关系
参考链接
https://www.zhihu.com/question/23031215
https://segmentfault.com/a/1190000018204798