JSON
JSON.parse(JSON.stringify(obj))
这种方法可以说是目前最简单直接的深拷贝方法了。
const obj1 = {
a: {
b: 'this is b'
},
age: 20
}
const obj2 = JSON.parse(JSON.stringify(obj1))
obj2.a.b = 'now, this is new value'
console.log(obj1) // { a: { b: 'this is b' }, age: 20 }
console.log(obj2) // { a: { b: 'now, this is new value' }, age: 20 }
简单粗暴......
虽然简单,但对于JSON.stringify(target)来说,存在着诸多局限:
-
如果
target有这三种属性的值:Undefined,Function,Symbol(值或属性),那么:若是target是对象,它们则会被忽略;若是target是数组,它们则会被序列化成null -
**拷贝
Date类型时,会变成字符串。**因为Date.prototype中定义了toJSON()方法(返回String)将其转为String。(toJSON()的返回值决定着目标什么值被序列化) -
对于**
NaN,-Infinity,Infinity**这三个值序列化的结果为null(null的值就是null) -
对包含循环引用的对象(
obj[key] = obj),则会报错
现在开始改进:
const a1 = {
age: 20,
b: {
toJSON() {
return 'this is toJSON'
}
},
time: new Date(),
unde: undefined,
func: () => ({}),
symb: Symbol(1),
num1: NaN,
num2: Infinity,
num3: -Infinity
}
function isNaN(target) {
return target !== target
}
function isInfinity(target) {
return typeof target === 'number' && !Number.isFinite(target)
}
function isDate(target) {
return target instanceof Date
}
function replacer(key, value) {
switch(true) {
case isNaN(value) || isInfinity(value) : // 考虑NaN / Infinity / -Infinity
return value.toString()
case value === undefined: // 考虑 undefined
return 'undefined'
case value === 'this is toJSON': // 验证toJSON方法是否运行在replacer之前:答案:确实是之前
console.log('****toJSON run in before replacer****')
return value
default:
return value
}
}
function reviver(key, value) {
switch(true) {
case value === 'NaN':
return NaN
case value === 'Infinity':
return Infinity
case value === '-Infinity':
return -Infinity
case value === 'undefined':
return undefined
case isDate(a1[key]):
return new Date(value)
default:
return value
}
}
const a2 = JSON.parse(JSON.stringify(a1, replacer), reviver)
console.log(a2)
由代码测试可得,对象的
toJSON方法会在传递给'replacer'之前执行,所以,不能在replacer中对Date类型进行处理,从而选择了上面这种笨拙的方式
我们粗暴而又笨拙的利用JSON.stringify的第二个参数'replacer'对NaN,Infinity/-Infinity,undefined进行了字符串化,从而解决了JSON.stringify对NaN,Infinity/-Infinity的*null化*,以及undefined的忽略。利用JSON.parse的第二个参数'reviver',对Date类型进行处理。
这种方式虽然充分利用了JSON.stringify,JSON.parse的参数,但始终不完美。我们需要对对象属性有个清晰的了解,确保其本身不含有'NaN', 'Infinity/-Infinity', 'undefined'这些字符串值。以及后面对Date的处理。同时,也处理不了其他缺陷
2、递归实现
先来个基础版:
function isPureObject(target) {
return Object.prototype.toString.call(target).slice(8, -1) === 'Object'
}
function deepClone(target) {
if(!isPureObject(target)) {
return target
}
const obj = {}
for(let key in target) {
obj[key] = deepClone(target[key])
}
return obj
}
基础版的缺陷是:
-
处理不了**
symbol属性**(并非值,值可以处理)。 -
只能处理普通引用类型,处理不了
Array, Date, RegExp, Error, Function这样的引用类型。 -
循环引用没有解决。
好,接下来就是逐个解决缺陷的时候了~
对数组的处理:
+ const obj = Array.isArray(target) ? [] : {}
- const obj = {}
对Date, RegExp,Error的处理:
// 很简单加上这段代码就行了
if(target instanceof Date) {
return new Date(target)
}
if(target instanceof RegExp) {
return new RegExp(target)
}
if(target instanceof Error) {
return new Error(target)
}
对Symbol属性的处理:
+ for(let key of Reflect.ownKeys(target))
- for(let key in target)
对循环引用的处理:
原理是,使用一个数据结构保存保存已经拷贝过的对象,如果对象已经拷贝过,则去这个数据结构里面取出,如果没拷贝过,执行拷贝的同时需要往这个数据结构里面存储。
选什么数据结构来保存拷贝过的对象呢?Map,WeakMap,Set,weakSet,Object怎么选?首先Object肯定不行,因为,我们的原理是保存obj => cloneObj的这种关系,Object的属性只能是String,Symbol。同时也排除了Set,WeakSet。对于Map,WeakMap来说,貌似都行。为了更精确一点,本文章选用WeakMap
在最终代码中体现出来
扩展
对于不可枚举属性和原型链上的属性:
const alldesc = Object.getOwnPropertyDescriptors(target)
obj = Object.create(Object.getPrototypeOf(target), alldesc)
最终代码:
function isPureObjectOrArray(target) {
return Object.prototype.toString.call(target).slice(8, -1) === 'Object' || Array.isArray(target)
}
function deepClone(target, hash = new WeakMap()) {
// 处理Date
if (target instanceof Date) {
return new Date(target)
}
// 处理RegExp
if (target instanceof RegExp) {
return new RegExp(target)
}
// 处理Error
if (target instanceof Error) {
return new Error(target)
}
if(!isPureObjectOrArray(target)) {
return target
}
if(hash.has(target)) return hash.get(target)
let obj
if(Array.isArray(target)) {
obj = []
} else {
const alldesc = Object.getOwnPropertyDescriptors(target)
obj = Object.create(Object.getPrototypeOf(target), alldesc)
}
hash.set(target, obj)
for (let key of Reflect.ownKeys(target)) {
obj[key] = deepClone(target[key], hash)
}
return obj
}