前言
对象的深浅拷贝,一直是老生常谈的话题,平台上的文章数量可谓是汗牛充栋,要从这块素材里找突破几乎是不可能。索性我就写一篇文章,积累一下自己的学习心得,以便后续复习的时候,能有一个比较清晰的思路。
定义
浅拷贝:将数据中所有的数据引用下来,并指向同一个存放地址,拷贝的数据修改之后,会对原数据产生副作用。
深拷贝:将数据中所有的数据拷贝下来,对拷贝之后的数据进行修改不会对原始数据产生副作用。
非深拷贝
业务中,很多时候你做的是浅拷贝,如果不影响业务逻辑,你可能不关心这些东西。
等号赋值
引用类型的等号赋值是最常见浅拷贝,如下所示:
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
是深拷贝方法,其实不然。它也是浅拷贝,只不过是第一级的原始类型的数据,不受牵连,引用类型还是会被篡改,我们用数据说话:
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)
打印结果如下:
绿色箭头代表原始类型,没有被篡改。红色箭头代表的是引用类型,都随着 newObj
的修改而变化。
... 扩展运算符
它比较特殊,如果要拷贝的对象,第一层是原始类型,则为深拷贝。如果是引用类型,则为浅拷贝,不妨做个小实验:
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)
obj
的 name
属性没有被改变,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)
确实没有被关联到,数据已经脱离了控制,但是函数 callback
么的了。
对象中存在时间对象 Date
var obj = {
name: 'Nick',
date: [new Date(1621259998866), new Date(1621259998866)],
};
var newObj = JSON.parse(JSON.stringify(obj))
obj
中的 date
内的时间对象被执行了。
对象中存在 RegExp、Error
var obj = {
name: 'Nick',
date: new RegExp('\\s+'),
};
var newObj = JSON.parse(JSON.stringify(obj));
obj.name = 'Chen'
拷贝之后,date
变成了一个空值。
对象中存在 undefined 值
var obj = {
name: undefiend
}
var newObj = JSON.parse(JSON.stringify(obj));
undefiend
在拷贝的过程中,被丢失了。
对象中存在 NaN、Infinity、-Infinity
var obj = {
name1: NaN,
name2: Infinity,
name3: -Infinity
}
var newObj = JSON.parse(JSON.stringify(obj))
直接全部变成 null
,不跟你嘻嘻哈哈,但是这种情况应该也不多。
对象中存在通过构造函数生产的对象
function Animal(name) {
this.name = name
}
var animal = new Animal('dog')
var obj = {
test: animal
}
var newObj = JSON.parse(JSON.stringify(obj))
直接就把构造函数给丢了,拷贝之后,直接指向了 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)
上述 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()
上述代码,在函数 test
内部声明 obj
对象,并将其以参数的形式,传递给 test1
方法。test1
内部的操作是给传进来的 _obj
参数赋值一个 a
属性,并且 return _obj
。
此时查看打印结果,obj
被也被添加了 a
属性,并且 _obj
全等于 obj
。这说明它们指向了同一个内存地址,就是 test
内的函数作用域。在《JavaScript 高级程序设计》第 86 页,对引用类型在函数之间的传递的知识有详细的分析。
利用这个原理,上述 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
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!