简介
对象类型在赋值的过程中其实是复制了地址,从而会导致改变了一方其他也都被改变的情况,解决这种问题的方法就是利用深/浅拷贝
浅拷贝可以直接使用扩展运算符(...)或者Object.assign直接处理
而深拷贝就没有现成的函数了
在业务开发中通常使用 JSON.parse(JSON.stringify(data))
应付大多数的深拷贝场景
但面试就一般考察使用递归去实现一个深拷贝
本文将较为详细的介绍每一种边界情况的实现方案
常见边界问题
循环引用
函数
正则
日期
symbol
多个键值引用了同一个对象,保持拷贝后的特性一致
- ...
1. 简单递归实现
不考虑边界问题,元素只有 值类型,obj,arr
function deepClone(obj) { if (!isObject(obj)) return obj if (Array.isArray(obj)) { const newObj = [] for (const v of obj) { newObj.push(isObject(v) ? deepClone(v) : v) } return newObj } if (isObject(obj)) { const newObj = {} Object.keys(obj).forEach(k => { const v = obj[k] newObj[k] = isObject(v) ? deepClone(v) : v }) return newObj } } const a = { name: 'xiaoming', id: 123131, info: { bd: '2020-01-01', cards: [{ q: 'q', w: [1, 2, 3], e: { c: 'c' } }] } } console.log(JSON.stringify(deepClone(a)));
2. 解决循环引用
众所周知 使用JSON进行深拷贝是无法解决对象的循环引用,如果出现了会直接报错
可以使用哈希表来解决此问题,将已存在的对象记录下来
对上面的deepclone稍加改动
function deepClone(obj) { const map = new WeakMap() const dp = (obj) => { if (!isObject(obj)) return obj // 解决循环引用 if (map.has(obj)) return map.get(obj) map.set(obj, Array.isArray(obj) ? [] : {}) if (Array.isArray(obj)) { const newObj = [] for (const v of obj) { newObj.push(isObject(v) ? dp(v) : v) } return newObj } if (isObject(obj)) { const newObj = {} Object.keys(obj).forEach(k => { const v = obj[k] newObj[k] = isObject(v) ? dp(v) : v }) return newObj } } return dp(obj) } const b = {}, c = {} b.next = c c.next = b console.log(deepClone(b)); // { next: { next: {} } }
3. 保持原对象的引用的特性
- 将已拷贝后的对象存储起来
- 经clone过的对象直接返回
function deepClone(obj) { const map = new WeakMap() const dp = (obj) => { if (!isObject(obj)) return obj // 已经clone过的对象直接返回 if (map.has(obj)) return map.get(obj) // 解决循环引用 map.set(obj, Array.isArray(obj) ? [] : {}) if (Array.isArray(obj)) { const newObj = [] for (const v of obj) { newObj.push(isObject(v) ? dp(v) : v) } // 将已拷贝后的对象存储起来 map.set(obj, newObj) return newObj } if (isObject(obj)) { const newObj = {} Object.keys(obj).forEach(k => { const v = obj[k] newObj[k] = isObject(v) ? dp(v) : v }) // 将已拷贝后的对象存储起来 map.set(obj, newObj) return newObj } } return dp(obj) } const obj = { a: 1 } const t3 = { a: obj, d: obj, f: { g: obj } } const tt3 = deepClone(t3) console.log(tt3); // { a: { a: 1 }, d: { a: 1 }, f: { g: { a: 1 } } } console.log(tt3.a === tt3.d); // true console.log(tt3.a === tt3.f.g); // true
4. 拷贝Symbol
这里最主要的是如何获取到到对象的Symbol键
获取对象的键的方案有以下几种
Reflect.ownKeys(target)
: 方法返回一个由目标对象自身的属性键组成的数组(包含普通键与Symbol键
)Object.getOwnPropertySymbols(target)
:返回一个给定对象自身的所有 Symbol 属性
的数组Object.getOwnPropertyNames(target)
:返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性
但不包括Symbol值作为名称的属性
)组成的数组Object.keys()
:返回一个由一个给定对象的自身可枚举属性
组成的数组,数组中属性名的排列顺序和正常循环遍历该对象时返回的顺序一致
综上
Reflect.ownKeys(target) // 等价于 Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))
再稍加改动一下我们的deepClone方法,这里我们直接采用Reflect.ownKeys(target)
替代原来的Object.keys(targer)
function deepClone(obj) { const map = new WeakMap() const dp = (obj) => { if (!isObject(obj)) return obj // 已经clone过的对象直接返回 if (map.has(obj)) return map.get(obj) // 解决循环引用 map.set(obj, Array.isArray(obj) ? [] : {}) if (Array.isArray(obj)) { const newObj = [] for (const v of obj) { newObj.push(isObject(v) ? dp(v) : v) } // 将已拷贝后的对象存储起来 map.set(obj, newObj) return newObj } if (isObject(obj)) { const newObj = {} // 使用Reflect.ownKeys替换 Reflect.ownKeys(obj).forEach(k => { const v = obj[k] newObj[k] = isObject(v) ? dp(v) : v }) // 将已拷贝后的对象存储起来 map.set(obj, newObj) return newObj } } return dp(obj) } const s1 = Symbol.for('s1') const s2 = Symbol.for('s2') const data = { [s1]: { name: 's1', age: 19 }, [s2]: [1, 2, 'string', { title: s1 }] } console.log(deepClone(data)); // { [Symbol(s1)]: { name: 's1', age: 19 }, // [Symbol(s2)]: [ 1, 2, 'string', { title: Symbol(s1) } ] }
5. 拷贝特殊对象Date/RegExp
对于特殊的对象,我们可以通过以下几步去处理
- 获取对象的构造函数
- 判断是否是指定的特殊对象
- 调用构造函数生成一个新的对象
实例化的对象可以通过.constructor
获取到其构造函数
我们修改上面的clone方法
function deepClone(obj) { const map = new WeakMap() const dp = (obj) => { if (!isObject(obj)) return obj // 已经clone过的对象直接返回 if (map.has(obj)) return map.get(obj) // 解决循环引用 map.set(obj, Array.isArray(obj) ? [] : {}) // 获取对象的构造函数 const fn = obj.constructor // 如果是正则 if (fn === RegExp) { return new RegExp(obj) } // 如果是日期 if (fn === Date) { return new Date(obj.getTime()) } if (Array.isArray(obj)) { const newObj = [] for (const v of obj) { newObj.push(isObject(v) ? dp(v) : v) } // 将已拷贝后的对象存储起来 map.set(obj, newObj) return newObj } if (isObject(obj)) { const newObj = {} // 使用Reflect.ownKeys替换 Reflect.ownKeys(obj).forEach(k => { const v = obj[k] newObj[k] = isObject(v) ? dp(v) : v }) // 将已拷贝后的对象存储起来 map.set(obj, newObj) return newObj } } return dp(obj) } const data = { today: new Date(), reg: /^abc$/ig } console.log(deepClone(data)); // { today: 2020-09-01T08:45:26.907Z, reg: /^abc$/gi }
拷贝函数
函数拷贝的方案在网上收集了一下五花八门,各种奇淫技巧,下面给大家列举一下哈哈
- 使用eval:
- eval(fn.toString()):只支持箭头函数
- new Function(‘return’+fn.toString())():不能将函数及其原始作用域一起克隆
- fn.bind():返回的新函数无法再通过bind去改变this指向
// 我这里就简单的使用.bind function deepClone(obj) { const map = new WeakMap() const dp = (obj) => { if (!isObject(obj)) return obj // 已经clone过的对象直接返回 if (map.has(obj)) return map.get(obj) // 解决循环引用 map.set(obj, Array.isArray(obj) ? [] : {}) // 获取对象的构造函数 const fn = obj.constructor // 如果是正则 if (fn === RegExp) { return new RegExp(obj) } // 如果是日期 if (fn === Date) { return new Date(obj.getTime()) } // 如果是函数 if (fn === Function) { return obj.bind({}) } if (Array.isArray(obj)) { const newObj = [] for (const v of obj) { newObj.push(isObject(v) ? dp(v) : v) } // 将已拷贝后的对象存储起来 map.set(obj, newObj) return newObj } if (isObject(obj)) { const newObj = {} // 使用Reflect.ownKeys替换 Reflect.ownKeys(obj).forEach(k => { const v = obj[k] newObj[k] = isObject(v) ? dp(v) : v }) // 将已拷贝后的对象存储起来 map.set(obj, newObj) return newObj } } return dp(obj) } const data = { today: new Date(), reg: /^abc$/ig, fn1: (a, b) => { console.log(this.today); console.log(a + b); }, fn2: function (a, b) { console.log(this.reg); console.log(a + b); } } const newData = deepClone(data) newData.fn1(1, 2) // undefined 3 newData.fn1.call({ today: '666' }, 1, 2) // undefined 3 newData.fn2(3, 4) // /^abc$/gi 7 newData.fn2.call({ reg: 123 }, 3, 4) // 123 7 const fn2 = newData.fn2 fn2.call({ reg: 'fn2Call' }, 2, 3) // fn2Call 5 const fn3 = fn2.bind({ reg: 'string' }) fn3(2, 3) // string 5
更详细的内容大家可以细品一下这篇文章
关于深拷贝完整实现 可以研究一下 lodash.cloneDeep,源码