第 6 题:请分别用深度优先思想和广度优先思想实现一个拷贝函数?
// 工具函数 let _toString = Object.prototype.toString let map = { array: 'Array', object: 'Object', function: 'Function', string: 'String', null: 'Null', undefined: 'Undefined', boolean: 'Boolean', number: 'Number' } let getType = (item) => { return _toString.call(item).slice(8, -1) } let isTypeOf = (item, type) => { return map[type] && map[type] === getType(item) }
深复制 深度优先遍历
let DFSdeepClone = (obj, visitedArr = []) => { let _obj = {} if (isTypeOf(obj, 'array') || isTypeOf(obj, 'object')) { let index = visitedArr.indexOf(obj) _obj = isTypeOf(obj, 'array') ? [] : {} if (~index) { // 判断环状数据 _obj = visitedArr[index] } else { visitedArr.push(obj) for (let item in obj) { _obj[item] = DFSdeepClone(obj[item], visitedArr) } } } else if (isTypeOf(obj, 'function')) { _obj = eval('(' + obj.toString() + ')'); } else { _obj = obj } return _obj }
广度优先遍历
let BFSdeepClone = (obj) => { let origin = [obj], copyObj = {}, copy = [copyObj] // 去除环状数据 let visitedQueue = [], visitedCopyQueue = [] while (origin.length > 0) { let items = origin.shift(), _obj = copy.shift() visitedQueue.push(items) if (isTypeOf(items, 'object') || isTypeOf(items, 'array')) { for (let item in items) { let val = items[item] if (isTypeOf(val, 'object')) { let index = visitedQueue.indexOf(val) if (!~index) { _obj[item] = {} //下次while循环使用给空对象提供数据 origin.push(val) // 推入引用对象 copy.push(_obj[item]) } else { _obj[item] = visitedCopyQueue[index] visitedQueue.push(_obj) } } else if (isTypeOf(val, 'array')) { // 数组类型在这里创建了一个空数组 _obj[item] = [] origin.push(val) copy.push(_obj[item]) } else if (isTypeOf(val, 'function')) { _obj[item] = eval('(' + val.toString() + ')'); } else { _obj[item] = val } } // 将已经处理过的对象数据推入数组 给环状数据使用 visitedCopyQueue.push(_obj) } else if (isTypeOf(items, 'function')) { copyObj = eval('(' + items.toString() + ')'); } else { copyObj = obj } } return copyObj }
测试
/**测试数据 */ // 输入 字符串String // 预期输出String let str = 'String' var strCopy = DFSdeepClone(str) var strCopy1 = BFSdeepClone(str) console.log(strCopy, strCopy1) // String String 测试通过 // 输入 数字 -1980 // 预期输出数字 -1980 let num = -1980 var numCopy = DFSdeepClone(num) var numCopy1 = BFSdeepClone(num) console.log(numCopy, numCopy1) // -1980 -1980 测试通过 // 输入bool类型 // 预期输出bool类型 let bool = false var boolCopy = DFSdeepClone(bool) var boolCopy1 = BFSdeepClone(bool) console.log(boolCopy, boolCopy1) //false false 测试通过 // 输入 null // 预期输出 null let nul = null var nulCopy = DFSdeepClone(nul) var nulCopy1 = BFSdeepClone(nul) console.log(nulCopy, nulCopy1) //null null 测试通过 // 输入undefined // 预期输出undefined let und = undefined var undCopy = DFSdeepClone(und) var undCopy1 = BFSdeepClone(und) console.log(undCopy, undCopy1) //undefined undefined 测试通过 //输入引用类型obj let obj = { a: 1, b: () => console.log(1), c: { d: 3, e: 4 }, f: [1, 2], und: undefined, nul: null } var objCopy = DFSdeepClone(obj) var objCopy1 = BFSdeepClone(obj) console.log(objCopy === objCopy1) // 对象类型判断 false 测试通过 console.log(obj.c === objCopy.c) // 对象类型判断 false 测试通过 console.log(obj.c === objCopy1.c) // 对象类型判断 false 测试通过 console.log(obj.b === objCopy1.b) // 函数类型判断 false 测试通过 console.log(obj.b === objCopy.b) // 函数类型判断 false 测试通过 console.log(obj.f === objCopy.f) // 数组类型判断 false 测试通过 console.log(obj.f === objCopy1.f) // 数组类型判断 false 测试通过 console.log(obj.nul, obj.und) // 输出null,undefined 测试通过 // 输入环状数据 // 预期不爆栈且深度复制 let circleObj = { foo: { name: function() { console.log(1) }, bar: { name: 'bar', baz: { name: 'baz', aChild: null //待会让它指向obj.foo } } } } circleObj.foo.bar.baz.aChild = circleObj.foo var circleObjCopy = DFSdeepClone(circleObj) var circleObjCopy1 = BFSdeepClone(circleObj) console.log(circleObjCopy, circleObjCopy1) // 测试通过?
关于深浅拷贝的问题博主已经在他的git上有文章说过了,我就不做多的叙述,这两个方法我认为主要区别在于对于深层次以及环状数据,用深度优先遍历递归去做容易爆栈,广度优先遍历我对环状数据进行了处理,已经存在过的对象会存在数组中,下次直接赋值即可,无需继续遍历。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(50)
公司gayhub怎么老是炸,感谢提醒,已更新,在处理环状数据时我将
visitedCopyQueue
错用成visitedQueue
了,导致上一次使用的数据是存在visitedQueue
里的引用导致深复制失败笔试时会不会纸上手写深度拷贝
代码有点难读
请问能不能解释一下环状结构数据呢?
解决深拷贝中的环问题
@atheist1
您好。我刚刚查看您的广度优先遍历 BFSdeepClone 函数。while递归中没有判断array的环状数据。
您可以将下面这段代码 添加到函数执行之前,来复现该问题
let a = [1,2,3,4]
let b = a
a.push(b)
obj.a = a;
@atheist1
因为刚刚发现的问题。我又一次尝试了下。深度优先遍历 DFSdeepClone 函数中 visitedArr 数组存放的都是原数组的引用类型。这会导致环状数据出现问题。
如下代码
let a = [1,2,3,4]
let b = a
a.push(b)
obj.a = a;
var objCopy = DFSdeepClone(obj)
obj.a[2] = 2;
objCopy 克隆对象中的a数组 第二次引用会和源数据保持一致
深度优先算法深拷贝有很多就不写了
广度优先算法深拷贝,实现了下,暂时没发现问题
这好像还是深度遍历
const test = {a: {b: {v: 1}}, c: {f: 2}, h: new Date()};
你可以再for循环内部打印key,看看路径
if (~index)
中的~是啥意思深度优先搜索的拷贝函数,没有考虑一些复杂的对象。
@atheist1 广度优先拷贝存在一个问题:
最外层是数组时,会变成对象
容我抖个机灵,深度优先json.parse(json.stringify(obj))
我自己用的这个
@atheist1
这边不应该是放入的原始值吧,应该是放入深拷贝之后的值_obj
circleObjCopy.foo.bar.baz.aChild === circleObjCopy.foo //false
测试用例最后一个,拷贝后的结果的循环引用不正确。
@atheist1
DFSdeepClone中通过if(~index)判断引用类型是否clone过不妥,直接判断index > -1不更好吗
谁能解释一下“环状数据”是什么意思? 我理解为 循环引用
这种不能满足所有场景,碰到函数,正则表达式,Symbol...就歇菜了
看不懂大佬的广度优先遍历
位运算 按位非
对任一数值 x 进行按位非操作的结果为 -(x + 1)。例如,~5 结果为 -6。
用在上面的意思就是
@WozHuang 你的方法好像没有考虑函数,函数拷贝过来的还是原函数的指针,应该同博主一样再做一层判断,eval赋值过来吧?
@panzhengtao 是的,环状数据指的就是子属性又去引用了父属性,导致了循环引用。
@atheist1 你的深度优先算法中,处理循环引用的流程感觉不太对,因为复制出来的结果与原对象的引用是同一个,即:
// 现在返回true,深拷贝按理应该是false,
circleObjCopy.foo.bar.baz.aChild === circleObj.foo
也许按 @WozHuang 的做法才正确,因为期望值应该是:
// 应该返回true
circleObjCopy.foo.bar.baz.aChild === circleObjCopy.foo
广度优先遍历深拷贝,只考虑了Date这特殊对象, 其余特殊对象判断方法一样 (已自测一部分数据,可能有些未覆盖到,望指出~)
嗯~原谅我是杠精,以Symbol为键的对象拷贝时会忽略该键的拷贝。
function deepClone(source, hash = new WeakMap()) {
if (!isObject(source)) return source;
var constructor = source.constructor;
if (constructor == RegExp) {
return new constructor(source)
} else if (constructor == Date) {
return new constructor(source.getTime())
} else if (constructor == Function) {
return eval('('+source.toString()+')')
} else {
if (hash.has(source)) {
return hash.get(source)
} else {
var target = new constructor;
hash.set(source, target);
for (var key in source) {
target[key] = deepClone(source[key], hash)
}
}
}
return target
}
高赞默写
引用类型只考虑Array,Object
补一个测试用例
··· js
var obj = {};
obj.target = obj;
太强了, 这个对环状结构的处理才是正确的
对象深拷贝大家可能最先想到的就是DFS递归属性,递归实现相对简单代码就不贴了,这里放下BFS实现,拷贝主要需要注意下循环引用和重复引用问题,这个可以引入一个缓存数组解决。
`/**
*/
function deepClone(obj) {
// 边界处理
if (obj.proto !== Object.prototype && obj.proto !== Array.prototype) {
return obj;
}
// 结果对象
let res = {};
// 队列内放对应层级的属性的对象或数组
const queue = [[res, obj]];
// 记录已经创建的引用和对应的旧对象引用,处理循环引用和重复引用
const hasCloneAttr = [
{
pre: obj, // 旧对象引用
now: res, // 新对象引用
},
];
while (queue.length > 0) {
const length = queue.length;
for (let i = 0; i < length; i++) {
const v = queue.shift();
const keys = Object.keys(v[1]);
for (const key of keys) {
// 对数组和对象进行深拷贝
if (
v[1][key].proto === Object.prototype ||
v[1][key].proto === Array.prototype
) {
let flag = false;
for (let item of hasCloneAttr) {
// 旧对象存在相同引用,则不需要再创建新的引用,直接使用之前创建引用就行
if (item.pre === v[1][key]) {
v[0][key] = item.now;
flag = true;
break;
}
}
if (!flag) {
v[0][key] = v[1][key].proto === Object.prototype ? {} : [];
hasCloneAttr.push({
pre: v[1][key],
now: v[0][key],
});
// 引用类型放入队列,下次依次取出处理
queue.push([v[0][key], v[1][key]]);
}
}
// 基本类型,直接赋值
else {
v[0][key] = v[1][key];
}
}
}
}
return res;
}`
circleObj.foo.bar=1;
执行后发现 circleObjCopy 也被改了
var o = {m: 2}
var obj = {
a: {b:o},
e: o
}
广度优先测试失败
我只深拷贝了 Object, Array,其他的非基本类型都是浅拷贝(如果处理Set什么的就太复杂了,题目用意应该是考察遍历树和重复引用吧)
DFS用常规的递归问题不大,需要注意下重复引用的问题,不用递归的话就用栈
BFS就用队列,整体代码倒是差不多
DFSdeepClone = (obj, visitedArr = [])
,这里visitedArr如果传参的话,子层的属性不能和同层的属性做比较。只有同层的环能检测出来。let b = {haha: 'haha'} let a = { a: {b: b}, c: {b: b} } const rs = DFSdeepClone(a) console.log(rs.a.b === rs.c.b) // false console.log(a.a.b === a.c.b) // true
顺便说明下,这里我对es6的symbol数据以及构造函数的拷贝都没做处理,想要了解的可以看大佬的
这篇文章【进阶4-4期】Lodash是如何实现深拷贝的
DFS没有对环状结构进行处理~
数值赋值本身就是复制,clone的时候需要复制的是array, object和function,这个代码里并没有针对function成员的复制。
DFSdeepClone 方法有问题吧 字符串 clone 出来不久变数组了
浏览器console 实验一下就错了
笔误:
map = {
number: 'Number'
}
@atheist1 老哥你代码最好添加代码高亮一下,这样阅读感很差。