前言

对象的深浅拷贝,一直是老生常谈的话题,平台上的文章数量可谓是汗牛充栋,要从这块素材里找突破几乎是不可能。索性我就写一篇文章,积累一下自己的学习心得,以便后续复习的时候,能有一个比较清晰的思路。

定义

浅拷贝:将数据中所有的数据引用下来,并指向同一个存放地址,拷贝的数据修改之后,会对原数据产生副作用。

深拷贝:将数据中所有的数据拷贝下来,对拷贝之后的数据进行修改不会对原始数据产生副作用。

非深拷贝

业务中,很多时候你做的是浅拷贝,如果不影响业务逻辑,你可能不关心这些东西。

等号赋值

引用类型的等号赋值是最常见浅拷贝,如下所示:

  1. var obj = {
  2. name: 'Nick'
  3. }
  4. var newObj = obj

此时你修改 newObj.name = 'Chen',则会使得 obj 也会跟着变化,这是因为声明的 obj 属于引用类型的变量,存在了全局作用域下的堆内存中。赋值给 newObj,只是将内存的地址赋值给了它,所以修改 newObj 的属性,也就是修改了堆内存中数据的属性,从而 obj 也会跟着改变。

  1. var obj = {
  2. name: 'Nick'
  3. }
  4. var newObj = obj
  5. newObj.name = 'Chen'
  6. console.log(obj.name) // 'Chen'
  7. console.log(newObj.name) // 'Chen'

Object.assign

你以为 Object.assign 是深拷贝方法,其实不然。它也是浅拷贝,只不过是第一级的原始类型的数据,不受牵连,引用类型还是会被篡改,我们用数据说话:

  1. var obj = {
  2. name: 'Nick',
  3. hobby: ['code', 'movie', 'travel', { a: 1 }]
  4. }
  5. var newObj = Object.assign({}, obj)
  6. newObj.name = 'Chen'
  7. newObj.hobby[0] = 'codeing'
  8. newObj.hobby[3].a = 2
  9. console.log('obj', obj)
  10. console.log('newObj', newObj)

打印结果如下:

image.png

绿色箭头代表原始类型,没有被篡改。红色箭头代表的是引用类型,都随着 newObj 的修改而变化。

… 扩展运算符

它比较特殊,如果要拷贝的对象,第一层是原始类型,则为深拷贝。如果是引用类型,则为浅拷贝,不妨做个小实验:

  1. var obj = {
  2. name: 'Nick',
  3. salary: {
  4. high: 1,
  5. mid: 2,
  6. low: 3
  7. }
  8. }
  9. var newObj = { ...obj }
  10. newObj.name = 'Chen'
  11. newObj.salary.high = 2
  12. console.log(obj)
  13. console.log(newObj)

image.png

objname 属性没有被改变,salary 中的 high 被改成了 2。

所以我们如果想用 ... 扩展运算符完成深拷贝,就得这样操作:

  1. var obj = {
  2. name: 'Nick',
  3. salary: {
  4. high: 1,
  5. mid: 2,
  6. low: 3
  7. }
  8. }
  9. var newObj = {
  10. ...obj,
  11. salary: {
  12. ...obj.salary
  13. }
  14. }

我觉得这样操作的人,肯定是有毛病。

JSON.parse + JSON.stringify

很多有志之士,会在代码中使用这种方式去做深拷贝。当然,多数业务场景中,这种方式还是比较香的,但是还是会有那么些情况,会出现大大小小的问题。

对象中存在函数:

  1. var obj = {
  2. name: 'Nick',
  3. hobby: ['code', 'movie', 'travel', { a: 1 }],
  4. callback: function() {
  5. console.log('test')
  6. }
  7. }
  8. var newObj = JSON.parse(JSON.stringify(obj))
  9. newObj.name = 'Chen'
  10. newObj.hobby[0] = 'codeing'
  11. newObj.hobby[3].a = 2
  12. console.log('obj', obj)
  13. console.log('newObj', newObj)

image.png

确实没有被关联到,数据已经脱离了控制,但是函数 callback 么的了。

对象中存在时间对象 Date

  1. var obj = {
  2. name: 'Nick',
  3. date: [new Date(1621259998866), new Date(1621259998866)],
  4. };
  5. var newObj = JSON.parse(JSON.stringify(obj))

image.png

obj 中的 date 内的时间对象被执行了。

对象中存在 RegExp、Error

  1. var obj = {
  2. name: 'Nick',
  3. date: new RegExp('\\s+'),
  4. };
  5. var newObj = JSON.parse(JSON.stringify(obj));
  6. obj.name = 'Chen'

image.png

拷贝之后,date 变成了一个空值。

对象中存在 undefined 值

  1. var obj = {
  2. name: undefiend
  3. }
  4. var newObj = JSON.parse(JSON.stringify(obj));

image.png

undefiend 在拷贝的过程中,被丢失了。

对象中存在 NaN、Infinity、-Infinity

  1. var obj = {
  2. name1: NaN,
  3. name2: Infinity,
  4. name3: -Infinity
  5. }
  6. var newObj = JSON.parse(JSON.stringify(obj))

image.png
直接全部变成 null,不跟你嘻嘻哈哈,但是这种情况应该也不多。

对象中存在通过构造函数生产的对象

  1. function Animal(name) {
  2. this.name = name
  3. }
  4. var animal = new Animal('dog')
  5. var obj = {
  6. test: animal
  7. }
  8. var newObj = JSON.parse(JSON.stringify(obj))

image.png

直接就把构造函数给丢了,拷贝之后,直接指向了 Object

诸如上述种种的情况,在真实开发环境中遇到的可能不是很多,但是你真的遇到了,在不知情的情况下,可能会耗费一些不必要的时间去找出问题所在。

狠狠滴深拷贝

首先,大可以使用 lodash.cloneDeep 这类工具实现深拷贝,有工具不用,哎,放着玩儿?

这里我要手动写一个深拷贝,从中可以学习到一些小知识点,爱看不看吧,我写给自己看。

  1. var obj = {
  2. name: 'Nick',
  3. date: [new Date(1621261792177)],
  4. callback: function() { console.log('shadiao') },
  5. link: undefined
  6. }
  7. function deepClone(origin) {
  8. if(origin === null) return null
  9. if(typeof origin !== 'object') return origin;
  10. if(origin.constructor === Date) return new Date(origin);
  11. // 接受两个参数,origin 是原对象
  12. var _target = origin.constructor() //保持继承链
  13. // 循环 origin
  14. for(var key in origin) {
  15. //不遍历其原型链上的属性
  16. if (origin.hasOwnProperty(key)) {
  17. // 如果 origin[key] 是一个引用类型的值,则进入递归逻辑
  18. if (typeof origin[key] === 'object' && origin[key] !== null) {
  19. // 进入递归,此时原始值就是 origin[key],被赋值的对象是 _target[key]
  20. // 注意,上述第一次声明的 _target 将会贯穿整个递归,后续所有的赋值,都将会被 return 到 _target
  21. _target[key] = deepClone(origin[key])
  22. } else {
  23. // 如果不是对象或数组,则进入此逻辑,直接赋值给 _target[key]
  24. _target[key] = origin[key]
  25. }
  26. }
  27. }
  28. // for...in 循环结束后,return 当前上下文的 _target 值
  29. return _target
  30. }
  31. const newObj = deepClone(obj)

image.png

上述 obj 对象的属性都被完整的拷贝下来了。

上述代码中,有一个关键步骤,如果理解了它,基本上你就理解为什么可以实现递归赋值,我们来看下面这段代码:

  1. function test() {
  2. var obj = {}
  3. const _obj = test1(obj)
  4. console.log('obj', obj)
  5. console.log('_obj', _obj)
  6. console.log(_obj === obj)
  7. }
  8. function test1(_obj) {
  9. _obj.a = 1
  10. return _obj
  11. }
  12. test()

image.png

上述代码,在函数 test 内部声明 obj 对象,并将其以参数的形式,传递给 test1 方法。test1 内部的操作是给传进来的 _obj 参数赋值一个 a 属性,并且 return _obj

此时查看打印结果,obj 被也被添加了 a 属性,并且 _obj 全等于 obj。这说明它们指向了同一个内存地址,就是 test 内的函数作用域。在《JavaScript 高级程序设计》第 86 页,对引用类型在函数之间的传递的知识有详细的分析。

image.png

利用这个原理,上述 deepClone 方法内部,执行递归的时候,所传进去的 _target[key] ,其实这个 _target 就是第一次执行 deepClone 的引用类型变量,后续递归操作对 _target[key] 的赋值,都将反映到最初的 _target。最后函数执行结束,return _target 便是最终递归深拷贝后的最终值。

总结

这个知识点非常细节,我不敢说会在业务开发中大量用到。但至少当你遇到这类问题的时候,你不会一头雾水、伤春悲秋,觉得自己不适合这个行业。再一次强调,基础知识很重要,不要小看这些平时不起眼的知识,真到了拼刺刀的时候,你一无所知。