1. 前言
很多前端er认为深拷贝很难,其实我觉得主要是网上很多文章代码虽然实现了深拷贝,但代码冗余度很高,确实是不利于阅读。
2. 难点在哪?
首先我给出下面这个对象
var obj = {
name: "muyiy",
book: {
title: "You Don't Know JS",
price: "45",
b: {
name: '小b',
age: 18,
next: {
name: '小二'
}
}
},
a1: undefined,
a2: null,
a3: 123
}
深拷贝与浅拷贝的区别就在于层级,浅拷贝只会复制一层的属性,深拷贝会完整的复制整个对象
我们不考虑任何特殊情况,实现一个简单的深拷贝(其实就是DFS遍历)
function cloneDeep(obj) {
var res = {}
for(var key in obj) {
if (obj.hasOwnProperty(key)) {
var value = obj[key]
if (typeof(value) == 'object') {
res[key] = cloneDeep(value)
} else {
res[key] = value
}
}
}
return res
}
这个函数是有问题的,遍历我们给出的obj对象时,a2: null
复制失败了,因为我们判断对象的函数很粗糙
引出第一个问题:
复制一些特殊对象(像Function,Date,Regexp…等)时要单独处理,对理解算法没有太大帮助
所以我们只对普通的对象进行处理
// 更精确的判断object函数
function isObject (obj) {
return Object.prototype.toString.call(obj) == '[object Object]'
}
然后我们看看还存在了什么问题?
然后看看其他比较常见的问题
- 递归爆栈
- 引用丢失
- 闭环
3. 递归太深了
当递归的层级过深的时候,会出现栈溢出的危险,一般我们考虑使用循环来代替递归来解决栈溢出的问题
先不管怎么去用循环重构我们的深拷贝函数
思考一下,深拷贝函数设计的流程是怎么样的?
只有两个步骤
- 遍历对象
- 遍历的过程中拷贝
ok,那按照步骤来,我们试着用循环去遍历对象,其实这就是BFS,广度优先搜索
其实与二叉树的层序遍历异曲同工,只是我们的对象不只两个节点,而是多个节点
下面的代码均采用ES6新语法
// BFS遍历对象
const cloneDeep = (obj) => {
// q是一个队列结构,实现BFS的基础
const q = [obj]
while (q.length > 0) {
const t = q.shift()
Object.keys(t).map(key => {
const value = t[key]
// console.log(`${key}: ${value}`)
if (isObject(value)) {
q.push(value)
}
})
}
return res
}
ok,遍历对象完成了,那怎么在遍历对象的时候,将拷贝对象的属性呢?
关键点在于我们怎么去复制边,这个边就是我们两个节点,原节点和克隆节点的映射关系
我们通过Map来映射我们的节点关系
在es6中有一个新的数据结构叫Map,多数静态语言中都会有,因为js中的object实质上就是一个类Map的结构,但是它的键值只能存字符串,所以它不能很好的描述两个节点的映射关系
const cloneDeep = (obj) => {
// res是我们的clone节点
const res = {}
const q = [obj]
const map = new Map()
// 建立对象节点与克隆节点的映射关系
map.set(obj, res)
while (q.length > 0) {
const t = q.shift()
Object.keys(t).map(key => {
const value = t[key]
if (isObject(value)) {
// 建立这个属性对象与克隆节点的映射关系
map.set(value, {})
q.push(value)
}
// 找到对应的克隆节点
const clone = map.get(t)
clone[key] = value
})
}
return res
}
这个深拷贝就解决了我们的递归爆栈问题,它其实并不难,相对于遍历树这步的代码而言,我们只增加了三行代码就解决了拷贝这一问题
4.引用丢失怎么办
引用丢失问题,obj.a
和obj.c
都引用到了c
对象
就像下面这样
var obj = {
a: {
name: c
}
b: c
}
那是不是会丢失引用呢?当然不会,引用本就不可能丢失,只是大多数人写的深拷贝函数有问题,需要额外的来增加代码解决引用丢失问题。
5. 闭环
闭环是个啥子情况呢?是指对象的引用链中,有一个属性引用到了之前的对象,于是引用链成了一个环形结构,也就是闭环
var a = {
b: {}
}
a.b.c = a
这种情况也很好解决,我们在更新映射关系的时候,我们加个判断,如果原节点在map中能找到,就不用创建新的克隆对象
最终版代码
const cloneDeep = (obj) => {
// res是我们的clone节点
const res = {}
const q = [obj]
const map = new Map()
// 建立对象节点与克隆节点的映射关系
map.set(obj, res)
while (q.length > 0) {
const t = q.shift()
Object.keys(t).map(key => {
const value = t[key]
if (isObject(value)) {
// 如果对象已经存在在表中,说明存在循环引用
if (map.has(value)) {
map.set(t, value)
} else {
// 建立这个属性对象与克隆节点的映射关系
map.set(value, {})
q.push(value)
}
}
// 找到对应的克隆节点
const clone = map.get(t)
clone[key] = value
})
}
return res
}
6. 最后
其实深拷贝下来,比较重要的就两个点
怎么遍历对象,怎么建立原节点和克隆节点之间的联系
最终一个简易的深拷贝实现下来也就二十行代码左右
写作不易~~如果对你有帮助的话,点个赞吧,Thanks♪(・ω・)ノ