一、简单实现
简单深拷贝可以分成2步,浅拷贝+递归
浅拷贝时增加判断属性值是否是对象,如果是对象就进行递归操作
这样就实现了深拷贝
首先,写出简单浅拷贝的代码:
1 | function shadowClone (source) { |
稍微改动,加上是否是对象的判断并在相应的位置使用递归就可以实现简单深拷贝:
1 | function deepClone1 (source) { |
但是这个简单版存在几个漏洞:
1.没有对传入参数进行校验,传入null时应该返回null而不是{}
2.对于对象的判断逻辑不严谨,typeof null === ‘object’
3.没有考虑到数组的拷贝问题
二、解决三个漏洞
对于是否是对象的判断,常用方法:
1 | function isObject(obj) { |
但是数组会被排除,因此要用typeof来处理,把数组保留下来
1 | function isObject(obj) { |
然后区分数组和对象
1 | let target = Array.isArray(source) ? [] : {}; |
深拷贝代码改造后如下:
1 | function deepClone2 (source) { |
三、解决循环引用
JSON.parse(JSON.stringify(obj))这种方式深拷贝遇到循环引用会报错:
// TypeError: Converting circular structure to JSON
es6 -> 哈希表
方法:循环检测,设置一个数组或者哈希表存储已经拷贝过的对象,当检测到对象已存在于哈希表时,取出该值并返回即可
1 | function deepClone3 (source, hash = new WeakMap()) { |
es5 -> 数组
1 | /* 数组中存储的对象结构: |
⚠️惊喜:存储已拷贝过的对象还解决了引用丢失的问题
1 | var obj1 = {}; |
四、拷贝Symbol
⚠️补课:如何检测出Symbol类型?
Symbol作为属性名。该属性不会出现在for…in ,for…of 循环中,也不会被Object.keys() 和
Object.getOwnPropertyNames() 返回。
可以通过以下两种方式检测:
- Object.getOwnPropertySymbols(…)
可以查找一个给定对象的符号属性时返回一个 symbol 类型的数组。注意,每个初始化的对象都是没有自己的 symbol 属性的,因此这个数组可能为空,除非你已经在对象上设置了 symbol 属性。
1 | let obj = {}; |
- Reflect.ownKeys(…)
返回一个由目标对象自身的属性键组成的数组,返回值等于
1 | Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target)) |
⚠️补充:Reflect其实就是换一种更加优雅的方式来操作对象
比如判断对象中是否有某个属性
1 | attr in obj // 传统做法 |
例如:获取window上绑定的所有事件
1 | let getAllWindowEvents = function (w) { |
1 | let obj = { |
拷贝Symbol思路:
检查有没有Symbol属性,如果找到则先遍历处理Symbol情况,然后再处理正常情况。
1.Object.getOwnPropertySymbols(…)方法:
1 | function deepClone4 (source, hash = new WeakMap()) { |
2.Reflect.ownKeys() 获取所有的键值,同时包括 Symbol,对 source 遍历赋值即可。
1 | function deepClone4 (source, hash = new WeakMap()) { |
Reflect.ownKeys() 这种方式的问题在于不能深拷贝原型链上的数据,因为返回的是目标对象自身的属性键组成的数组。如果想深拷贝原型链上的数据用 for..in 就可以了。
⚠️扩展:es6使用展开语法构造字面量数组和对象
1.展开语法-字面量数组
- 不再需要组合使用 push, splice, concat 等方法
- 返回的是新数组,对新数组修改之后不会影响到旧数组,类似于 arr.slice()。
- 展开语法和 Object.assign() 行为一致, 执行的都是浅拷贝。
1 | var a = [[1], [2], [3]]; |
这里 a 是多层数组,b 只拷贝了第一层,对于第二层依旧和 a 持有同一个地址,所以对 b 的修改会影响到 a。
2.展开语法-字面量对象
将已有对象的所有可枚举属性拷贝到新构造的对象中,类似于 Object.assign() 方法。
1 | var obj1 = { foo: 'bar', x: 42 }; |
Object.assign() 函数会触发 setters,而展开语法不会。有时候不能替换或者模拟 Object.assign() 函数,因为会得到意想不到的结果,如下:
1 | var obj1 = { foo: 'bar', x: 1 }; |
这里实际上是将多个解构变为剩余参数( rest ),然后再将剩余参数展开为字面量对象
五、破解递归爆栈
上面深拷贝的方法都用到了递归,可能会出现一个爆栈问题,错误提示如下:
1 | // RangeError: Maximum call stack size exceeded |
解决办法:循环+栈代替递归
1 | function deepClone5 (x) { |
结合哈希表破解循环引用,得到终极版深拷贝如下:
1 | function deepClone6 (source) { |
测试一下终极版:
1 | var obj1 = {}; |
完美~
参考文章:
https://juejin.im/post/5c45112e6fb9a04a027aa8fe
https://segmentfault.com/a/1190000016672263