详参:如何写出一个惊艳面试官的深拷贝? 作者:ConardLi
浅拷贝
创建一个新对象,这个对象拷贝原始对象属性值。如果属性值是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址。
深拷贝
将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。
手写深拷贝
丐版 -- 简单,但拷贝其他引用类型、拷贝函数、循环引用中有很大缺陷
JSON.parse(JSON.stringify());
01. 基础版本
// 浅拷贝易写出
function clone(target) {
let cloneTarget = {};
for (const key in target) {
cloneTarget[key] = target[key];
}
return cloneTarget;
}
// 深拷贝,不确定待拷贝的对象有多少层深度,可用递归解决
// 如果是原始类型,无需拷贝,直接返回;
// 引用类型,创建一个新对象,遍历需要克隆的对象,将需克隆对象的属性执行深拷贝后依次添加到新对象上。
function clone(target) {
if (typeof target === 'object') {
let cloneTarget = {};
for (const key in target) {
cloneTarget[key] = clone(target[key]);
}
return cloneTarget;
} else {
return target;
}
}
同样有缺陷,没考虑到数组
02.兼容数组
function clone(target) {
if (typeof target === 'object') {
let cloneTarget = Array.isArray(target) ? [] : {};
for (const key in target) {
cloneTarget[key] = clone(target[key]);
}
return cloneTarget;
} else {
return target;
}
}
03.兼容对象循环引用
即对象的属性间接或直接的引用了自身的情况
// 测试用例
const target = {
field1: 1,
field2: undefined,
field3: {
child: 'child'
},
field4: [2, 4, 8]
}
target.target = target;
解决循环引用问题,可额外开辟一个存储空间存储当前对象和拷贝对象的对应关系。
当需要拷贝当前对象时,先去存储空间中找是否拷贝过这个对象。
有 -- 直接返回;没有 -- 继续拷贝
function clone(target, map = new Map()) {
if (typeof target === 'object') {
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;
} else {
return target;
}
}
可用 WeakMap 代替 Map ,画龙点睛
Map -- 对象间存在强引用关系,只有手动 obj = null,它才会被垃圾回收机制进行回收
WeakMap -- 弱引用对象,垃圾回收机制会自动帮我们回收
function clone(target, map = new WeakMap()) {
// ...
}
04. 性能优化
for...in 在遍历时效率非常低
执行效率 while > for > for...in
// 用 while 实现一个通用的 forEach 遍历,iteratee是遍历的回调函数
function forEach(array, iteratee) {
let index = -1;
const length = array.length;
while( ++index < length) {
iteratee(array[index], index);
}
return array;
}
function clone(target, map = new WeakMap()) {
if (typeof target === 'object') {
const isArray = Array.isArray(target);
let cloneTarget = isArray ? [] : {};
if (map.get(target)) {
return map.get(target);
}
map.set(target, cloneTarget);
const keys = isArray ? undefined : Object.keys(target);
forEach(keys || target, (value, key) => {
if (keys) {
key = value;
}
cloneTarget[key] = clone(target[key], map);
});
return cloneTarget;
} else {
return target;
}
}
05. 其他数据类型
上述只考虑了普通的 object 和 array 两种数据类型,引用类型还有很多
首先,判断是否为引用类型,还需考虑 function 和 null 两种特殊的数据类型
function isObject(target) {
const type = typeof target;
return target !== null && (type === 'object' || type === 'function');
}
if (!isObject(target)) {
return target;
}
// ...
获取数据类型
可以使用 toString 来获取准确的引用类型
可直接调用 Object 原型上未被覆盖的 toString() 方法,使用 call 来改变 this 指向来达到效果。
function getType(target) {
return Object.prototype.toString.call(target);
}
| value //Object.prototype.toString.call(value) | 结果 |
|---|---|
| true | [Object Boolean] |
| 123 | [Object Number] |
| 'cc' | [Object String] |
| null | [Object Null] |
| undefined | [Object Undefined] |
| Symbol() | [Object Symbol] |
| {} | [Object Object] |
| function(){} | [Object Function] |
| [] | [Object Array] |
| new Error() | [Object Error] |
| new RegExp() | [Object RegExp] |
| Math | [Object Math] |
| JSON | [Object JSON] |
| new document | [Object RegExp] |
| window | [Object global] |
可将上面的集中类型分为:可继续遍历的类型 、不可继续遍历的类型
可继续遍历的类型
object 、 array 、Map、Set 等
获取它们的初始化数据,可通过拿到 constructor 来获取
function getInit(target) {
const Ctor = target.constructor;
return new Ctor();
}
改写 clone 函数
const mapTag = '[object Map]';
const setTag = '[object Set]';
const arrayTag = '[object Array]';
const objectTag = '[object Object]';
const argsTag = '[object Arguments]';
const deepTag = [mapTag, setTag, arrayTag, objectTag, argsTag];
function clone(target, map = new WeakMap()) {
// 克隆原始类型
if (!isObject(target)) {
return target;
}
// 初始化
const type = getType(target);
let cloneTarget;
if (deepTag.includes(type)) {
cloneTarget = getInit(target, type);
} 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;
}
// 克隆对象和数组
const keys = type === arrayTag ? undefined : Object.keys(target);
forEach(keys || target, (value, key) => {
if (keys) {
key = value;
}
cloneTarget[key] = clone(target[key], map);
});
return cloneTarget;
}
不可继续遍历的类型
Bool 、 Number 、 String 、 Date 、 Error 这几种可直接用构造函数和原始数据创建一个新对象。
const boolTag = '[object Boolean]';
const dateTag = '[object Date]';
const numberTag = '[object Number]';
const stringTag = '[object String]';
const symbolTag = '[object Symbol]';
const errorTag = '[object Error]';
const regexpTag = '[object RegExp]';
const funcTag = '[object Function]';
function cloneOtherType(target, type) {
const Ctor = target.constructor;
switch (type) {
case boolTag:
case numberTag:
case stringTag:
case errorTag:
case dateTag:
return new Ctor(target);
case regexpTag:
return cloneReg(target);
case symbolTag:
return cloneSymbol(target);
default:
return null;
}
}
function cloneSymbol(target) {
return Object(Symbol.prototype.valueOf.call(target));
}
function cloneReg(target) {
const reFlags = /\w*$/;
const result = new target.constructor(target.source, reFlags.exec(target));
result.lastIndex = target.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);
}
}