循序渐进深拷贝
js对象拷贝是面试官喜欢问的点,细细探究下来,深拷贝涉及了很多细节。
# 浅拷贝
拷贝一个对象,这个对象有着原始对象属性值的一份精确拷贝。
如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,
所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
我们可以很容易实现一个浅copy:
function clone(obj) {
let res = {}
Object.keys(obj).forEach(k=>res[k]=obj[k])
return res
}
或者使用
function clone(obj) {
return Object.assign({}, obj)
}
Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。
# 深拷贝
将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象
简单实现:
function deepClone(obj) {
return JSON.parse(JSON.stringify(obj))
}
下面这个例子可以直观看出区别
let obj = {b: {c: {}};
let obj1 = clone(obj); // 浅拷贝
obj1.b.c === obj.b.c; // true
let obj2 = deepClone(obj); // 深拷贝
obj2.b.c === obj.b.c; // false
上面讨巧的JSON序列化的方法有这几个问题:
- date对象clone回来会变成字符串
- RegExp、Error对象clone回来会变成空对象
- 函数,undefined类型会丢失
- NaN、Infinity和-Infinity会变为null
- 循环引用会抛出错误
let obj = {
num:1,
date:new Date(),
regex:new RegExp('\d'),
err:new Error('err'),
fn:new Function(),
undf:undefined,
nan:NaN,
inf:Infinity,
map:new Map(),
wmap:new WeakMap(),
arr:Array.from({length:3}).map(o=>1),
obArr:[{a:1,b:{c:{}}},{b:[1,23]}],
deep:{a:{b:{c:{d:{e:{f:{g:{}}}}}}}}
}
let res = JSON.parse(JSON.stringify(obj))
console.log(res)
/*
res: {
arr: (3) [1, 1, 1]
date: "2021-04-08T07:32:10.996Z"
err: {}
inf: null
map: {}
nan: null
num: 1
regex: {}
wmap: {}
__proto__: Object
}
*/
// 循环引用
obj.obj = obj
JSON.parse(JSON.stringify(obj)) // Uncaught TypeError: Converting circular structure to JSON
对于复杂场景,我们需要一个更完善等deepClone;
# 循序渐进
我们按最简单浅copy版本一步一步完善。
# 基础版本
function deepClone(obj) {
let res = {};
Object.keys(obj).forEach(k=>res[k]=obj[k])
return res;
}
这个版本实现了浅拷贝,对象多层嵌套会有问题。
# 考虑对象引用
const isType = type => terget=> `[object ${type}]`===Object.prototype.toString.call(terget)
const isObject = isType('Object')
function deepClone(obj) {
if (isObject(obj)) {
let res = {};
Object.keys(obj).forEach(k=>{
res[k] = deepClone(obj[k])
})
return res
} else {
return obj;
}
}
let res = deepClone(obj)
/*
res: {
arr: (3) [1, 1, 1]
date: Thu Apr 08 2021 16:54:36 GMT+0800 (China Standard Time) {}
deep: {a: {…}}
err: Error: err at snippet:///deepClone:5:10
fn: ƒ anonymous( )
inf: Infinity
map: Map(0) {}
nan: NaN
num: 1
regex: /d/
obArr: (2) [{…}, {…}]
undf: undefined
wmap: WeakMap {}
}
*/
// obj.obArr[0].b === res.obArr[0].b // true
使用递归实现了层级深入遍历,接下来我们加入数组判断
# 考虑数组
const isType = type => terget=> `[object ${type}]`===Object.prototype.toString.call(terget)
const isObject = isType('Object')
const isArray = isType('Array')
function deepClone(obj) {
if (isObject(obj)||isArray(obj)) {
let res = isArray?[]:{};
Object.keys(obj).forEach(k=>{
res[k] = deepClone(obj[k])
})
return res
} else {
return obj;
}
}
let res = deepClone(obj)
/*
res: {
arr: (3) [1, 1, 1]
date: Thu Apr 08 2021 16:54:36 GMT+0800 (China Standard Time) {}
deep: {a: {…}}
err: Error: err at snippet:///deepClone:5:10
fn: ƒ anonymous( )
inf: Infinity
map: Map(0) {}
nan: NaN
num: 1
regex: /d/
obArr: (2) [{…}, {…}]
undf: undefined
wmap: WeakMap {}
}
*/
// obj.obArr[0].b === res.obArr[0].b // false
// 循环引用会造成无限循环
obj.obj=obj
let res = deepClone(obj)
// Uncaught RangeError: Maximum call stack size exceeded
# 考虑循环引用
假如一个对象a,a下面的两个键值都引用同一个对象b,经过深拷贝后,a的两个键值会丢失引用关系,从而变成两个不同的对象。这叫做引用丢失。原对象引用关系与复制后不一致,这当然也是不对的。
我们这样解决,将需要复制的对象开辟一片空间存起来,复制时判断是否已经复制过,复制过直接取用,否则继续。循环引用也如此解决。我们使用map存储,map的key可以是任意类型,而且key的比较是基于 sameValueZero (opens new window) 算法。 存取性能也都非常好。
const isType = type => terget=> `[object ${type}]`===Object.prototype.toString.call(terget)
const isObject = isType('Object')
const isArray = isType('Array')
function deepClone(obj,map=new Map()) {
if (isObject(obj)||isArray(obj)) {
let res = isArray?[]:{};
if(map.get(obj)){
return map.get(obj)
}
map.set(obj,res)
Object.keys(obj).forEach(k=>{
res[k] = deepClone(obj[k],map)
})
return res
} else {
return obj;
}
}
obj.obj=obj
let res = deepClone(obj)
// res.obj.obj.obj === obj // true
# 考虑其他类型
以上我们仅仅处理了object和array两种引用类型,实际上还有其他很多类型需要考虑。
# null 和 function
null直接返回,function也可以直接返回,因为克隆函数是没有实际应用场景。如果非要clone函数,还得区分普通函数与箭头函数。可以prototype来区分下箭头函数和普通函数,箭头函数是没有prototype
let x = {a:()=>{},b:function(){}}
console.log(x)
/*
x: {
a: ()=>{}
arguments: (...)
caller: (...)
length: 0
name: "a"
__proto__: ƒ ()
[[FunctionLocation]]: VM2310:1
[[Scopes]]: Scopes[2]
b: ƒ ()
arguments: null
caller: null
length: 0
name: "b"
prototype: {constructor: ƒ}
__proto__: ƒ ()
[[FunctionLocation]]: VM2310:1
[[Scopes]]: Scopes[2]
__proto__: Object
}
*
*
*
* */
函数复制方法
let fun = {
a: (v)=>{
console.log(v)
}
,
b: function(v) {
console.log(v + v)
}
}
// 很多函数库都是用这个方法
let cloneFn = (fn) =>{
return new Function('return ' + fn.toString())()
}
let a1 = cloneFn(fun.a)
let b1 = cloneFn(fun.b)
// 这样好像不需要区分普通函数和剪头函数了
# Bool、Number、String、Date、Error
这几种类型我们都可以直接用构造函数和原始数据创建一个新对象
克隆Symbol类型
function cloneSymbol(targe) {
return Object(Symbol.prototype.valueOf.call(targe));
}
// 克隆正则:
function cloneReg(targe) {
const reFlags = /\w*$/;
const result = new targe.constructor(targe.source, reFlags.exec(targe));
result.lastIndex = targe.lastIndex;
return result;
}
function cloneOtherType(targe, type) {
const Ctor = targe.constructor;
switch (type) {
case boolTag:
case numberTag:
case stringTag:
case errorTag:
case dateTag:
return new Ctor(targe);
case regexpTag:
return cloneReg(targe);
case symbolTag:
return cloneSymbol(targe);
default:
return null;
}
}
综上已实现一个较为完备的深拷贝,还有其他很多类型处理都可以按部就班if分支😄。
# 相关资料
lodash baseClone (opens new window)