Open yygmind opened 5 years ago
递归爆栈可以使用setTimeout分片,同步执行分片推入队列中,而且不影响后面的代码执行, target[key] = await new Promise(resolve => { setTimeout(async() => { resolve(await clone(obj[key])) }, 0); })
解决拷贝的对象是数组的时候,
var obj1 = [1, 2, 3, { a: 1, b: 2, c: [1, 2, 3] }];
var deepClone = function(obj) {
var root = Array.isArray(obj) ? [] : {};
var nodeList = [
{
parent: root,
key: undefined,
data: obj,
},
];
while (nodeList.length) {
let node = nodeList.pop(),
parent = node.parent,
k = node.key,
data = node.data;
let result = parent;
if (typeof k !== 'undefined') {
result = parent[k] = Array.isArray(data)? [] : {};
}
for (let key in data) {
if (data.hasOwnProperty(key)) {
if (typeof data[key] === 'object') {
nodeList.push({
parent: result,
key,
data: data[key],
});
} else {
result[key] = data[key];
}
}
}
}
return root;
};
最后一步,const node = loopList.pop();
应该用 shift 方法才算是广度优先吧?先进先出。 @yygmind
好像并没有说到如何解决函数、date、正则等问题的方法 @yygmind
let a = {
aa: undefined,
b: 1,
c: function () {
console.log(this.b);
},
dd: '',
e: [[1], [1, 2], { gg: 321 }],
q: null,
y: {
m: 1
},
[Symbol('bbq')]: 1
};
Object.defineProperty(a, 'method1', {
value: function () {
alert("Non enumerable property");
},
enumerable: false
});
function isObj(e) {
return typeof e === 'object' && e !== null;
};
function deepCopy(obj, arr = new WeakMap()) {
if (!isObj(obj)) return obj;
if (arr.has(obj)) return arr.get(obj);
let result = Array.isArray(obj) ? [] : {};
arr.set(obj, result);
Reflect.ownKeys(obj).forEach(key => {
if (isObj(obj[key])) {
result[key] = deepCopy(obj[key], arr);
} else {
result[key] = obj[key];
}
})
return result;
};
a.b = a;
let a1 = deepCopy(b)
console.log(a1, a);
var a = [1,2,3] var b = JSON.parse(JSON.stringify(a)) 完事
var a = [1,2,3] var b = JSON.parse(JSON.stringify(a)) 完事
new Date(),undefined,function,正则怎么办
好像还缺一个平级引用的问题
let a = {}, b = {} a.b = b a.c = a.b
a.circleRef = a;这样操作。栈就溢出了
我有一个地方不是很理解,就是为什么要判断Object.prototype.hasOwnProperty.call(source, key)) ,key不是source循环拿到的吗?
我有一个地方不是很理解,就是为什么要判断Object.prototype.hasOwnProperty.call(source, key)) ,key不是source循环拿到的吗?
for in 会遍历出自身和原型可枚举属性;Object.prototype.hasOwnProperty用来检测确定自身属性
我有一个地方不是很理解,就是为什么要判断Object.prototype.hasOwnProperty.call(source, key)) ,key不是source循环拿到的吗?
可以看下这篇文章,里面有例子;https://juejin.cn/post/6961253439477284877
因为key 有些数据类型拿不到,比如symbol
------------------ 原始邮件 ------------------ 发件人: @.>; 发送时间: 2021年5月13日(星期四) 中午11:24 收件人: @.>; 抄送: @.>; @.>; 主题: Re: [yygmind/blog] 【进阶4-3期】面试题之如何实现一个深拷贝 (#29)
我有一个地方不是很理解,就是为什么要判断Object.prototype.hasOwnProperty.call(source, key)) ,key不是source循环拿到的吗?
— You are receiving this because you commented. Reply to this email directly, view it on GitHub, or unsubscribe.
想请教下 循环引用的处理 为什么一定要用weakMap 可以使用Map吗
这是来自QQ邮箱的假期自动回复邮件。你好,工作繁忙,无法保证及时查看,但我尽快给你回复。
你的来信我一收到 谢谢哈 ~~~~
存收到,谢谢!
放心吧,我已经收到啦。
好折磨,取消这个isuue的关注了。
函数复制的时候 为什么使用new Function() , 使用eval可以吗
这是来自QQ邮箱的假期自动回复邮件。你好,工作繁忙,无法保证及时查看,但我尽快给你回复。
引言
上篇文章详细介绍了浅拷贝
Object.assign
,并对其进行了模拟实现,在实现的过程中,介绍了很多基础知识。今天这篇文章我们来看看一道必会面试题,即如何实现一个深拷贝。本文会详细介绍对象、数组、循环引用、引用丢失、Symbol 和递归爆栈等情况下的深拷贝实践,欢迎阅读。第一步:简单实现
其实深拷贝可以拆分成 2 步,浅拷贝 + 递归,浅拷贝时判断属性值是否是对象,如果是对象就进行递归操作,两个一结合就实现了深拷贝。
根据上篇文章内容,我们可以写出简单浅拷贝代码如下。
上面代码是浅拷贝实现,只要稍微改动下,加上是否是对象的判断并在相应的位置使用递归就可以实现简单深拷贝。
一个简单的深拷贝就完成了,但是这个实现还存在很多问题。
1、没有对传入参数进行校验,传入
null
时应该返回null
而不是{}
2、对于对象的判断逻辑不严谨,因为
typeof null === 'object'
3、没有考虑数组的兼容
第二步:拷贝数组
我们来看下对于对象的判断,之前在【进阶3-3期】有过介绍,判断方案如下。
但是用在这里并不合适,因为我们要保留数组这种情况,所以这里使用
typeof
来处理。改动过后的 isObject 判断逻辑如下。
所以兼容数组的写法如下。
第三步:循环引用
我们知道
JSON
无法深拷贝循环引用,遇到这种情况会抛出异常。1、使用哈希表
解决方案很简单,其实就是循环检测,我们设置一个数组或者哈希表存储已拷贝过的对象,当检测到当前对象已存在于哈希表中时,取出该值并返回即可。
测试一下,看看效果如何。
完美!
2、使用数组
这里使用了
ES6
中的WeakMap
来处理,那在ES5
下应该如何处理呢?也很简单,使用数组来处理就好啦,代码如下。
现在已经很完美的解决了循环引用这种情况,那其实还是一种情况是引用丢失,我们看下面的例子。
引用丢失在某些情况下是有问题的,比如上面的对象 obj2,obj2 的键值 a 和 b 同时引用了同一个对象 obj1,使用 cloneDeep2 进行深拷贝后就丢失了引用关系变成了两个不同的对象,那如何处理呢。
其实你有没有发现,我们的 cloneDeep3 已经解决了这个问题,因为只要存储已拷贝过的对象就可以了。
完美!
第四步:拷贝
Symbol
这个时候可能要搞事情了,那我们能不能拷贝 Symol 类型呢?
当然可以,不过
Symbol
在ES6
下才有,我们需要一些方法来检测出Symble
类型。方法一:
Object.getOwnPropertySymbols(...)
方法二:
Reflect.ownKeys(...)
对于方法一可以查找一个给定对象的符号属性时返回一个
?symbol
类型的数组。注意,每个初始化的对象都是没有自己的symbol
属性的,因此这个数组可能为空,除非你已经在对象上设置了symbol
属性。(来自MDN)对于方法二返回一个由目标对象自身的属性键组成的数组。它的返回值等同于
Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))
。(来自MDN)方法一
思路就是先查找有没有
Symbol
属性,如果查找到则先遍历处理Symbol
情况,然后再处理正常情况,多出来的逻辑就是下面的新增代码。测试下效果
完美!
方法二
这里使用了
Reflect.ownKeys()
获取所有的键值,同时包括Symbol
,对 source 遍历赋值即可。写到这里已经差不多了,我们再延伸下,对于
target
换一种写法,改动如下。在改动 1 中,返回一个新数组或者新对象,获取到源对象之后就可以如改动 2 所示传入 target 遍历赋值即可。
Reflect.ownKeys()
这种方式的问题在于不能深拷贝原型链上的数据,因为返回的是目标对象自身的属性键组成的数组。如果想深拷贝原型链上的数据怎么办,那用for..in
就可以了。我们再介绍下两个知识点,分别是构造字面量数组时使用展开语法和构造字面量对象时使用展开语法。(以下代码示例来源于 MDN)
1、展开语法之字面量数组
这是
ES2015 (ES6)
才有的语法,可以通过字面量方式, 构造新数组,而不再需要组合使用push
,splice
,concat
等方法。这里的使用方法和参数列表的展开有点类似。
返回的是新数组,对新数组修改之后不会影响到旧数组,类似于
arr.slice()
。展开语法和
Object.assign()
行为一致, 执行的都是浅拷贝(即只遍历一层)。这里 a 是多层数组,b 只拷贝了第一层,对于第二层依旧和 a 持有同一个地址,所以对 b 的修改会影响到 a。
2、展开语法之字面量对象
这是
ES2018
才有的语法,将已有对象的所有可枚举属性拷贝到新构造的对象中,类似于Object.assign()
方法。Object.assign()
函数会触发 setters,而展开语法不会。有时候不能替换或者模拟Object.assign()
函数,因为会得到意想不到的结果,如下所示。这里实际上是将多个解构变为剩余参数(
rest
),然后再将剩余参数展开为字面量对象.第五步:破解递归爆栈
上面四步使用的都是递归方法,但是有一个问题在于会爆栈,错误提示如下。
那应该如何解决呢?其实我们使用循环就可以了,代码如下。
由于篇幅问题就不过多介绍了,详情请参考下面这篇文章。
本期思考题
参考
进阶系列目录
交流
进阶系列文章汇总如下,内有优质前端资料,觉得不错点个star。
我是木易杨,公众号「高级前端进阶」作者,跟着我每周重点攻克一个前端面试重难点。接下来让我带你走进高级前端的世界,在进阶的路上,共勉!