狠狠滴深拷贝

前言

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

定义

浅拷贝:将数据中全部的数据引用下来,并指向同一个存放地址,拷贝的数据修改以后,会对原数据产生反作用。前端

深拷贝:将数据中全部的数据拷贝下来,对拷贝以后的数据进行修改不会对原始数据产生反作用。java

非深拷贝

业务中,不少时候你作的是浅拷贝,若是不影响业务逻辑,你可能不关心这些东西。面试

等号赋值

引用类型的等号赋值是最多见浅拷贝,以下所示:数组

var obj = {
  name: 'Nick'
}

var newObj = obj
复制代码

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

var obj = {
  name: 'Nick'
}

var newObj = obj

newObj.name = 'Chen'

console.log(obj.name) // 'Chen'
console.log(newObj.name) // 'Chen'
复制代码

Object.assign

你觉得 Object.assign 是深拷贝方法,其实否则。它也是浅拷贝,只不过是第一级的原始类型的数据,不受牵连,引用类型仍是会被篡改,咱们用数听说话:markdown

var obj = {
  name: 'Nick',
  hobby: ['code', 'movie', 'travel', { a: 1 }]
}

var newObj = Object.assign({}, obj)

newObj.name = 'Chen'
newObj.hobby[0] = 'codeing'
newObj.hobby[3].a = 2

console.log('obj', obj)
console.log('newObj', newObj)
复制代码

打印结果以下:函数

image.png

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

... 扩展运算符

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

var obj = {
  name: 'Nick',
  salary: {
  	high: 1,
    mid: 2,
    low: 3
  }
}

var newObj = { ...obj }

newObj.name = 'Chen'
newObj.salary.high = 2

console.log(obj)
console.log(newObj)
复制代码

image.png

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

因此咱们若是想用 ... 扩展运算符完成深拷贝,就得这样操做:

var obj = {
  name: 'Nick',
  salary: {
  	high: 1,
    mid: 2,
    low: 3
  }
}

var newObj = {
	...obj,
  salary: {
  	...obj.salary
  }
}
复制代码

我以为这样操做的人,确定是有毛病。

JSON.parse + JSON.stringify

不少有志之士,会在代码中使用这种方式去作深拷贝。固然,多数业务场景中,这种方式仍是比较香的,可是仍是会有那么些状况,会出现大大小小的问题。

对象中存在函数:

var obj = {
  name: 'Nick',
  hobby: ['code', 'movie', 'travel', { a: 1 }],
  callback: function() {
    console.log('test')
  }
}

var newObj = JSON.parse(JSON.stringify(obj))

newObj.name = 'Chen'
newObj.hobby[0] = 'codeing'
newObj.hobby[3].a = 2

console.log('obj', obj)
console.log('newObj', newObj)
复制代码

image.png

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

对象中存在时间对象 Date

var obj = {
  name: 'Nick',
  date: [new Date(1621259998866), new Date(1621259998866)],
};

var newObj = JSON.parse(JSON.stringify(obj))
复制代码

image.png

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

对象中存在 RegExp、Error

var obj = {
  name: 'Nick',
  date: new RegExp('\\s+'),
};

var newObj = JSON.parse(JSON.stringify(obj));
obj.name = 'Chen'
复制代码

image.png

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

对象中存在 undefined 值

var obj = {
	name: undefiend
}

var newObj = JSON.parse(JSON.stringify(obj));
复制代码

image.png

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

对象中存在 NaN、Infinity、-Infinity

var obj = {
  name1: NaN,
  name2: Infinity,
  name3: -Infinity
}

var newObj = JSON.parse(JSON.stringify(obj))
复制代码

image.png 直接所有变成 null,不跟你嘻嘻哈哈,可是这种状况应该也很少。

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

function Animal(name) {
	this.name = name
}

var animal = new Animal('dog')

var obj = {
	test: animal
}

var newObj = JSON.parse(JSON.stringify(obj))
复制代码

image.png

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

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

狠狠滴深拷贝

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

这里我要手动写一个深拷贝,从中能够学习到一些小知识点,爱看不看吧,我写给本身看。

var obj = {
  name: 'Nick',
  date: [new Date(1621261792177)],
  callback: function() { console.log('shadiao') },
  link: undefined
}

function deepClone(origin) {
  if(origin === null) return null 
  if(typeof origin !== 'object') return origin;
  if(origin.constructor === Date) return new Date(origin); 
	// 接受两个参数,origin 是原对象
  var _target = origin.constructor() //保持继承链
  // 循环 origin
	for(var key in origin) {
    //不遍历其原型链上的属性
    if (origin.hasOwnProperty(key)) {
    	// 若是 origin[key] 是一个引用类型的值,则进入递归逻辑
      if (typeof origin[key] === 'object' && origin[key] !== null) {
        // 进入递归,此时原始值就是 origin[key],被赋值的对象是 _target[key]
        // 注意,上述第一次声明的 _target 将会贯穿整个递归,后续全部的赋值,都将会被 return 到 _target
        _target[key] = deepClone(origin[key])
      } else {
        // 若是不是对象或数组,则进入此逻辑,直接赋值给 _target[key]
        _target[key] = origin[key]
      }
    }
  }
  // for...in 循环结束后,return 当前上下文的 _target 值
  return _target
}

const newObj = deepClone(obj)


复制代码

image.png

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

上述代码中,有一个关键步骤,若是理解了它,基本上你就理解为何能够实现递归赋值,咱们来看下面这段代码:

function test() {
	var obj = {}
  const _obj = test1(obj)
  console.log('obj', obj)
  console.log('_obj', _obj)
	console.log(_obj === obj)
}

function test1(_obj) {
	_obj.a = 1
  return _obj
}

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 即是最终递归深拷贝后的最终值。

总结

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

往期好文推荐

打通任督二脉的前端环境变量 — env 点赞数👍 228

Vite 2.0 + React + Ant Design 4.0 搭建开发环境 点赞数👍 385

面不面试的,你都得懂原型和原型链 点赞数👍 593

Vue 3 和 Webpack 5 来了,手动搭建的知识该更新了 点赞数👍 521

换一个角度分析,网页性能优化 点赞数👍 200

你好,谈谈你对前端路由的理解 点赞数👍 625

之前我没得选,如今我只想用 Array.prototype.reduce 点赞数👍 588

无处不在的发布订阅模式 —— 此次必定 点赞数👍 164

聊聊 JSX 和虚拟 DOM 点赞数👍 110

相关文章
相关标签/搜索