「引言」
❝臣闻求木之长者,必固其根本;欲流之远者,必浚其泉源。
---- 魏征 《谏太宗十思疏》
❞
或许你会问到,网上已经把深浅拷贝(算一个面试的高频考点了吧)的文章都快写烂了,为什么自己还要重新操刀写一遍呢!?
❝首先,一些文章,讲不清也道不明本质;另外,确实有很优秀的人写的很是生动,让我直接看到了风景,却不知道沿途是不是也有自己错过的美景,唯有尝试过,才会真正成为自己的~
❞
首先,我们先来看一张笔者整理的脑图,梳理一下~
希望通过本文的总结,你会有以下几点收获:
- 什么是深浅拷贝?他们与赋值有何区别?
- 浅拷贝的实现方式有哪些?
- 深拷贝的实现方式有哪些?
本章节直接从拷贝开始说起,对于基本数据类型,引用数据类型之前的区别,可以看看上面的思维导图
引用数据类型拷贝
我们从以下三个方面来看看这块的内容
- 赋值
- 浅拷贝
- 深拷贝
赋值
引用类型的赋值是传址。其引用指向堆中的同一个对象,因此操作其中一个对象,另一个对象是会跟着一起变的。
举个栗子:
let lucy = {
name: 'lucy',
age: 23
}
let lilei = lucy
lilei.name = 'lilei'
lilei.age = 24
console.log('lucy', lucy) // lucy {name: "lilei", age: 24}
console.log('lilei', lilei) // lilei {name: "lilei", age: 24}
上面栗子中可以看出来,修改了 lilei 的数据,lucy也会跟着变。这是初学者(笔者也曾这样)经常犯的一个错,后来深刻理解了对象内存的重要性!改掉了这个恶习~
那么我们该如何不让彼此之间不影响呢?
接下来我们引出了 拷贝这个概念,拷贝又分深拷贝和浅拷贝。
来看一看具体是什么和相关区别吧。
「注意:」
- 对于基本数据类型而言,并没有深浅拷贝的区别
- 深浅拷贝都是对于引用数据类型而言的
- 如果我们要赋值对象的所有属性都不是引用类型时,我们可以使用浅拷贝,遍历并复制,最后返回一个对象
「本质&使用场景」:都是复杂对象,就是说对象的属性还是对象
浅拷贝
「本质」:只复制一层对象,当对象的属性是引用类型时,实质复制的是其引用,当引用值指向发生改变时也会跟着改变
「原理」:遍历并复制,最后返回一个对象
来动手实现一个简单的浅拷贝吧
// 实现浅拷贝 for in
let shallowCopy = (obj) => {
let rst = {}
for (let key in obj) {
// 只复制本身拥有的属性(非继承过来的属性)
if (obj.hasOwnProperty(key)) {
rst[key] = obj[key]
}
}
return rst
}
let lucy = {
name: 'lucy',
age: 23,
hobby: ['running', 'swimming']
}
let lilei = shallowCopy(lucy)
lilei.name = 'lilei'
lilei.age = 24
lilei.hobby[0] = 'reading'
console.log('lucy', lucy)
// lucy {name: "lucy", age: 23, hobby: ['reading', 'swimming']}
console.log('lilei', lilei)
// lilei {name: "lilei", age: 24, hobby: ['reading', 'swimming']}
我们可以看到,当对象的属性是引用类型时,实质复制的是其引用,当引用值指向发生改变时也会跟着改变。
深拷贝
「实质」:深拷贝出来的对象会互不影响
「原理」:对对象中子对象进行递归拷贝
我们下面会手写一个深拷贝哈~接着往下看,会有不一样的收货!
浅拷贝的实现方式
平常用到的浅拷贝有以下几种(欢迎评论补充,互相分享进步)
- Object.assign()
- 扩展运算符(...)
- Array.prototype.slice()
Object.assign()
首先 Object.assign(target, source)
可以把n个源对象拷贝到目标对象中去(这不是本节重点讨论的内容,先一笔带过)
然后呢,Object.assign 是 ES6新增的对象方法,那么它到底是一个深拷贝还是一个浅拷贝的方法呢?
告诉你一个绝招吧(小点声)!
「拷贝对象时,第一级属性是深拷贝,以后级别浅拷贝」
举个栗子你就知道了
let lucy = {
name: 'lucy',
age: 23,
hobby: ['running', 'swimming']
}
let lilei = Object.assign({}, lucy)
lilei.name = 'lilei'
lilei.age = 24
lilei.hobby[0] = 'reading'
console.log('lucy', lucy)
// lucy {name: "lucy", age: 23, hobby: ['reading', 'swimming']}
console.log('lilei', lilei)
// lilei {name: "lilei", age: 24, hobby: ['reading', 'swimming']}
可以看出这个和咱们上面实现的那个浅拷贝的结果是一样的。
还是那句话:「拷贝对象时,第一级属性是深拷贝,以后级别浅拷贝」
是不是简简单单呢~
扩展运算符(...)
这个和 Object.assign
一样,我们来看个栗子验证一下
let lucy = {
name: 'lucy',
age: 23,
hobby: ['running', 'swimming']
}
let lilei = {...lucy}
lilei.name = 'lilei'
lilei.age = 24
lilei.hobby[0] = 'reading'
console.log('lucy', lucy)
// lucy {name: "lucy", age: 23, hobby: ['reading', 'swimming']}
console.log('lilei', lilei)
// lilei {name: "lilei", age: 24, hobby: ['reading', 'swimming']}
哦~一毛一样啊和上面。
Array.prototype.slice()
说到这个方法,我第一次看见的时候是在看 vue
源码的时候,那个时候真是涨见识(姿势)了
话不多说,看一下就知道
// Dep notify 方法
Dep.prototype.notify = function notify() {
var subs = this.subs.slice()
// ...
}
利用了slice()
方法会返回一个新的数组对象,但也是一个浅拷贝的方法。
即「拷贝对象时,第一级属性是深拷贝,以后级别浅拷贝」
看一个具体的栗子
let a1 = [1, 2, [3, 4]]
let a2 = a1.slice()
a2[1] = 3
a2[2][0] = 5
console.log('a1', a1) // a1 (3) [1, 2, [5, 4]]
console.log('a2', a2) // a2 (3) [1, 3, [5, 4]]
是不是验证了这个道理呢~
同时也要去「注意」 concat
这些会返回一个新的数组对象方法等,避免造成一些工作开发者不必要的困扰~
深拷贝的实现方式
深拷贝拷贝出来的对象互不影响,但深拷贝相比于浅拷贝速度会比较慢且开销会较大,所以考虑清楚数据结构有几层,不是很复杂的数据结构建议浅拷贝来节省性能。
看一种最简单的深拷贝实现方式
JSON.parse(JSON.stringify())
**原理:**能将json的值json化
就是指纯JSON数据,不包含循环引用,循环引用会报错
拿之前的栗子改造一下看看有哪些需要注意的地方
let lucy = {
name: 'lucy',
age: 23,
hobby: ['running', 'swimming'],
say: function() {
return this.name
},
other: undefined
}
let lilei = JSON.parse(JSON.stringify(lucy))
lilei.name = 'lilei'
lilei.age = 24
lilei.hobby[0] = 'reading'
console.log('lucy', lucy)
// lucy {
// name: 'lucy',
// age: 23,
// hobby: ['running', 'swimming'],
// say: function() {
// return this.name
// },
// other: undefined
// }
console.log('lilei', lilei)
// lilei {age: 24, hobby: ['reading', swimming], name: 'lilei'}
可以看出来这个方法还是挺强大的。
但是也能发现一些问题
- 会忽略 undefined Symbol
- 不能序列化函数
- 不能解决循环引用的对象
- 不能处理正则
- 不能正确处理 new Date() (转换成时间戳可以拷贝)
此外,深拷贝的其他方法还有 jQuery.extend()
以及一些三方库实现的深拷贝 lodash.cloneDeep()
等等。大家感兴趣可自行了解,继续深造~
重头戏,面试常考,手写一个深拷贝,哈哈哈是不是就等这个呢~
我们改造一下上面的浅拷贝
递归实现深拷贝
// 判断边界, null 这个特殊情况
let isObject = obj => typeof obj === 'object' && obj !== null
// 递归实现深拷贝
let deepClone = (obj) => {
// 先判断是数组还是对象
let newObj = Array.isArray(obj) ? [] : {}
if (isObject(obj)) {
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
if (isObject(obj[key])) {
// 递归调用每一层
newObj[key] = deepClone(obj[key])
} else {
newObj[key] = obj[key]
}
}
}
}
return newObj
}
let aa = {
name: 'aa',
car: ['宝马', '奔驰'],
driver: function () { },
age: undefined
}
let bb = deepClone(aa) // 全部拷贝了一份
bb.name = 'bb'
bb.age = 20
bb.driver = 'xxx'
console.log(bb)
// { name: 'bb', car: [ '宝马', '奔驰' ], driver: 'xxx', age: 20 }
console.log(aa)
// { name: 'aa', car: [ '宝马', '奔驰' ], driver: function() {}, age: undefined }
可以看出来,咱们这个递归实现的深拷贝,规避掉了 上面 JSON.parse(JSON.stringify())
的一些弊端。但是还存在一些问题
- 循环检测的问题
- 拷贝一个Symbol类型的值又该怎么解决?
- 如何解决递归爆栈的问题
哈希表
针对于循环检测,我们可以使用哈希检测的方法,比如设置一个数组或者是已经拷贝的对象,当检测到对象已经存在哈希表时,就去除该值。
let isObject = obj => typeof obj === 'object' && obj !== null;
let deepClone = (source, hash = new WeakMap()) => {
if (!isObject(source)) return source // 非对象返回自身
if (hash.has(source)) return hash.get(source) // 新增检测, 查哈希表
let target = Array.isArray(source) ? [] : {}
hash.set(source, target) // 设置哈希表值
for (let key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = isObject(source[key]) ? deepClone(source[key], hash) : source[key]; // 传入哈希表
}
}
return target
}
let obj = {
a: 1,
b: {
c: 2,
d: 3
}
}
obj.a = obj.b;
obj.b.c = obj.a;
let clone_obj = deepClone(obj)
console.log(clone_obj)
上面实现有点难度,如果未能一下看透,不妨先跳过,完成之前的那个深拷贝就够了,当然,我喜欢不惧困难的人~
剩下的两个就交给喜欢深度思考的人来去头脑风暴一下吧。
最后总结一下
和原数据是否指向同一个对象 | 第一层数据为基本数据类型 | 原数据中包含子对象 | |
---|---|---|---|
赋值 | 是 | 改变会影响原数据 | 改变会影响原数据 |
浅拷贝 | 否 | 改变「不会」影响原数据 | 改变会影响原数据 |
深拷贝 | 是 | 改变「不会」影响原数据 | 改变「不会」影响原数据 |
写在最后
❝享受过程带来的喜悦,学会去克服自己的缺点!
❞