浅拷贝
创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
深拷贝
讲一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。
乞丐版实现方式
JSON.parse(JSON.stringify(obj))
我们想要深拷贝一个对象,使用最多的方法应该就是这个了,写法非常简单,而且可以应对大部分应用场景,但是它有个很大的缺陷,比如拷贝其他引用类型、拷贝函数、循环引用等。
基础版本
function clone(target){
let cloneTarget={}
for(const key in target){
cloneTarget[key]=target[key]
}
return cloneTarget
}
创建一个新对象,遍历需要克隆的对象,将需要克隆对象的属性一次添加到新对象上,返回新对象。
如果是深拷贝的话,考虑到我们要拷贝的对象不知道有多少层的深度,我们可以用递归来解决问题。
function clone(target) {
if (typeof target !== 'object') {
return target
} else {
let cloneTarget = {}
for (const key in target) {
cloneTarget[key] = clone(target[key])
}
return cloneTarget
}
}
这是一个最基础版本的深拷贝,这段代码可以让你向面试官展示你可以用递归解决问题,但是还是有很多缺陷,比如没有考虑到数组。
考虑数组
function clone(target) {
if (typeof target !== 'object') {
return target
} else {
let cloneTarget = Array.isArray(target)?[]:{}
for (const key in target) {
cloneTarget[key] = clone(target[key])
}
return cloneTarget
}
}
循环引用问题
const target = {
field1: 1,
field2: undefined,
field3: {
child: 'child'
},
field4: [2, 4, 8]
};
target.target = target;
如果使用上面函数去直接该对象,会直接报错,原因很明显,因为递归进入了死循环导致栈内存溢出。
解决循环引用问题,我们可以额外开辟一个存储空间,用来存储当前对象和拷贝对象的对应关系,当需要拷贝对象当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝。
这个存储空间,需要可以存储key-value形式的数据,且key可以是一个引用类型,我们可以选择Map这种数据结构。
- 检查
map中有无克隆过的对象 - 有 - 直接返回
- 没有 - 将当前对象作为
key,克隆对象作为value进行存储 - 继续克隆
function clone(target,map=new WeakMap()) {
if (typeof target !== 'object') {
return target
} else {
let cloneTarget = Array.isArray(target)?[]:{}
if(map.get(target)){
return map.get(target)
}
map.set(target,cloneTarget)
for (const key in target) {
cloneTarget[key] = clone(target[key],map)
}
return cloneTarget
}
}
解释下这里为什么使用WeakMap而不是事用Map的原因。
WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。
什么是弱引用呢?
在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。一个对象若只被弱引用所引用,则被被人是不可访问(或弱可访问),因为可能在任何时刻被回收
性能优化
在上面的代码中,我们遍历数组和对象都使用了for in这种方式,实际上for in在遍历时效率是非常低的。
function clone(target, map = new WeakMap()) {
if (typeof target !== 'object') {
return target
} else {
let cloneTarget = Array.isArray(target) ? [] : {}
if (map.get(target)) {
return map.get(target)
}
map.set(target, cloneTarget)
Object.keys(target).forEach(key => {
cloneTarget[key] = clone(target[key], map)
})
return cloneTarget
}
}
其他数据类型
首先,判断是否为引用类型,我们还需要考虑function和null两种特殊的数据类型
function isObject(target) {
const type = typeof target;
return target !== null && (type === 'object' || type === 'function');
}
获取数据类型
我们可以使用toString来获取准确的引用类型
function getType(target) {
return Object.prototype.toString.call(target);
}
抽离出一些常用的数据类型方便后面使用
const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const numberTag = '[object Number]';
const regexpTag = '[object RegExp]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
以上类型中,可以简单分为两类:
- 可以继续遍历的类型
- 不可以继续遍历的类型
可继续遍历的类型
上面我们已经考虑的object、array都属于可以继续遍历的类型,因为他们内部都还可以存储其他数据类型的数据,另外还有Map,Set等都是可以继续遍历的类型。
function getInit(target) {
const Ctor = target.constructor;
return new Ctor();
}
下面,我们该写clone函数,对可继续遍历的数据类型进行处理
function isObject(target) {
const type = typeof target;
return target !== null && (type === 'object' || type === 'function');
}
function getType(target) {
return Object.prototype.toString.call(target);
}
function getInit(target) {
const Ctor = target.constructor;
return new Ctor();
}
const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const numberTag = '[object Number]';
const regexpTag = '[object RegExp]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const deepTag = [mapTag, setTag, arrayTag, objectTag]
function clone(target, map = new WeakMap()) {
if (!isObject(target)) {
return target
}
const type = getType(target)
let cloneTarget
if(deepTag.includes(type)){
cloneTarget=getInit(target)
}
if (map.get(target)) {
return map.get(target)
}
map.set(target, cloneTarget)
//克隆set
if(type===setTag){
target.forEach(value=>{
cloneTarget.add(clone(value,map))
})
return cloneTarget
}
//克隆map
if(type===mapTag){
target.forEach((value,key)=>{
cloneTarget.set(key,clone(value,map))
})
return cloneTarget
}
//克隆对象数组
Object.keys(target).forEach(key => {
cloneTarget[key] = clone(target[key], map)
})
return cloneTarget
}
const target = {
field1: 1,
field2: undefined,
field3: {
child: 'child'
},
field4: [2, 4, 8],
field5:new Set([1,2,3]),
field6:new Map([[111,222],[222,333]])
};
target.target = target;
const obj = clone(target)
console.log(obj)
不可继续遍历的类型
Bool、Number、String、String、Date、Error这几种类型我们都可以直接用构造函数和原始数据创建一个新对象:
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;
}
}
克隆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 cloneFunction(func) {
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{)/;
const funcString = func.toString();
if (func.prototype) {
console.log('普通函数');
const param = paramReg.exec(funcString);
const body = bodyReg.exec(funcString);
if (body) {
console.log('匹配到函数体:', body[0]);
if (param) {
const paramArr = param[0].split(',');
console.log('匹配到参数:', paramArr);
return new Function(...paramArr, body[0]);
} else {
return new Function(body[0]);
}
} else {
return null;
}
} else {
return eval(funcString);
}
}
执行完整代码
//判断数据类型
function isObject(target) {
const type = typeof target;
return target !== null && (type === 'object' || type === 'function');
}
//获取数据类型
function getType(target) {
return Object.prototype.toString.call(target);
}
//初始化数据
function getInit(target) {
const Ctor = target.constructor;
return new Ctor();
}
//克隆其他数据类型
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);
case funcTag:
return cloneFunction(targe);
default:
return null;
}
}
//克隆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 cloneFunction(func) {
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{)/;
const funcString = func.toString();
if (func.prototype) {
console.log('普通函数');
const param = paramReg.exec(funcString);
const body = bodyReg.exec(funcString);
if (body) {
console.log('匹配到函数体:', body[0]);
if (param) {
const paramArr = param[0].split(',');
console.log('匹配到参数:', paramArr);
return new Function(...paramArr, body[0]);
} else {
return new Function(body[0]);
}
} else {
return null;
}
} else {
return eval(funcString);
}
}
const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const errorTag = '[object Error]';
const numberTag = '[object Number]';
const regexpTag = '[object RegExp]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const funcTag = '[object Function]';
const deepTag = [mapTag, setTag, arrayTag, objectTag]
function clone(target, map = new WeakMap()) {
if (!isObject(target)) {
return target
}
const type = getType(target)
let cloneTarget
if (deepTag.includes(type)) {
cloneTarget = getInit(target)
} else {
return cloneOtherType(target, type)
}
if (map.get(target)) {
return map.get(target)
}
map.set(target, cloneTarget)
//克隆set
if (type === setTag) {
target.forEach(value => {
cloneTarget.add(clone(value, map))
})
return cloneTarget
}
//克隆map
if (type === mapTag) {
target.forEach((value, key) => {
cloneTarget.set(key, clone(value, map))
})
return cloneTarget
}
//克隆对象数组
Object.keys(target).forEach(key => {
cloneTarget[key] = clone(target[key], map)
})
return cloneTarget
}
const target = {
field1: 1,
field2: undefined,
field3: {
child: 'child'
},
field4: [2, 4, 8],
field5: new Set([1, 2, 3]),
field6: new Map([
[111, 222],
[222, 333]
]),
bool: new Boolean(true),
num: new Number(2),
str: new String(2),
symbol: Object(Symbol(1)),
date: new Date(),
reg: /\d+/,
error: new Error(),
func1: () => {
console.log('code秘密花园');
},
func2: function (a, b) {
return a + b;
}
};
target.target = target;
const obj = clone(target)
console.log(obj)