如果按下面这么写的话,面试官看完眉头一皱,"好了下一个~"
function deepClone(obj) {
if (typeof obj !== "object" || obj === null) {
// 如果不是复杂数据类型,直接返回
return obj;
}
let cloneObj = Array.isArray(obj) ? [] : {};
// 遍历对象或数组
for (let key in obj) {
// 如果是对象的属性,递归调用deepClone
if (obj.hasOwnProperty(key)) {
cloneObj[key] = deepClone(obj[key]);
}
}
return cloneObj;
}
在上述的简单深拷贝实现中,只考虑了数组和普通对象,并没有考虑其他对象,且没有考虑循环引用的问题,可能带来死循环。
目录解读:
重点注意:
- 考虑null和基本数据类型
- 考虑循环引用
- 考虑日期、正则、弱引用对象
- 考虑Map、Set对象
- 考虑函数、箭头函数
- 考虑不可枚举对象
- 深拷贝应用场景
循环引用
使用弱引用WeakMap,防止内存泄漏
判断是否存在该引用,存在则直接返回。
// visited = new WeakMap (当函数参数传递)
if (visited.has(obj)) {
return visited.get(obj);
}
日期、正则、弱引用对象
判断并重新new实例一个它们的构造函数
const constructor = obj.constructor;
if(/^(RegExp|Date|WeakMap|WeakSet)/.test(constructor)) {
const res = new constructor(obj);
visited.set(obj, res); // 避免循环引用
return res; // 直接返回
}
Map、Set对象
const constructor = obj.constructor;
// Map
if (constructor === Map) {
const map = new Map();
visited.set(obj, map); // 避免循环引用
obj.forEach((value, key) => {
map.set(key, _completeDeepClone(value, visited)); // 递归拷贝深层对象
});
return map;
}
// Set
if (constructor === Set) {
const set = new Set();
visited.set(obj, set); // 避免循环引用
obj.forEach((value) => {
set.add(_completeDeepClone(value, visited)); // 递归拷贝深层对象
});
return set;
}
函数、箭头函数
注:虽然考虑函数没有任何意义,但是也体现了自身能力的一部分嘛
箭头函数和函数的区别
箭头函数没有自己的 constructor 属性,它继承了外部作用域的 constructor 属性。,不能用new实例化,因为它不存在原型对象,没有prototype属性,且没有自己的this,它的this继承于外层作用域。
我们可以拿prototype属性来判断
function isFunc(fn) {
return fn instanceof Function && !fn.hasOwnProperty('prototype');
}
那箭头函数如何拷贝?
因为箭头函数没有自己的this,且没有构造函数和原型链,我们可以直接使用toString()
方法转为字符串,然后直接使用eval
或者new Function()
重新创建内存保存。
简单介绍一下
eval
和new Function
newFunction
new Function
是 JavaScript 中的一个内置函数,它可以动态创建一个函数(运行时生成,不会有函数提升)。
const func = new Function('arg1', 'arg2', 'return arg1 + arg2;');
console.log(func(1, 2)); // 3
需要注意的是,使用
new Function
创建的函数的作用域链是相对较为简单的,它只包含函数本身和全局作用域。
eval
eval
它的作用是将传入的字符串参数作为代码进行解析和执行。
const res = eval(`${obj.toString()}`);
// 或者
const res = new Function(`return ${obj.toString}`)();
函数:分为普通函数和构造函数
考虑到原型链继承,和构造函数的自身属性问题,我们无法直接使用上述方法直接执行字符串创建新内存。
但是可以用寄生组合式继承来继承原型链和方法。
const res = function(...args) {
return obj.apply(res, args);
}
visited.set(obj, res); // 避免循环引用
// 继承自身静态属性(只能通过构造函数自身访问),比如 obj.a = 1
Object.keys(obj).forEach(key => res[key] = obj[key]);
// 寄生组合式继承
res.prototype = Object.create(obj.prototype);
res.prototype.constructor = res; // 构造函数指向自己
return res;
不可枚举对象
包括 Symbol
类型和设置了 enumerable
为 false 的成员
对于 Symbol
类型,我们可以用 Object.getOwnPropertySymbols(obj)
来获取,
而对于除了 Symbol
类型的成员,我们都可以用 Object.getOwnPropertyNames(obj)
来获取属性名。
那怎么获取它们的数据描述符属性呢?
直接使用 Object.getOwnPropertyDescriptor
可以获取属性描述符。如下:
const { value, writable, enumerable, configurable } = Object.getOwnPropertyDescriptor(obj, key);
并用 Object.defineProperty
定义就行。
实现:
// 处理普通对象和数组(数组也可以存在键值对)
const result = Array.isArray(obj) ? [] : {};
visited.set(obj, result); // 避免循环引用
// 获取对象的所有属性名,包括不可枚举属性
const props = Object.getOwnPropertyNames(obj); // 不可枚举类型
const symbolProps = Object.getOwnPropertySymbols(obj); // Symbol类型
props.concat(symbolProps).forEach(key => {
const descriptor = Object.getOwnPropertyDescriptor(obj, key);
if(descriptor) {
const { value, writable, enumerable, configurable } = descriptor;
Object.defineProperty(result, key, {
value: _completeDeepClone(value, visited),
writable, enumerable, configurable
})
}
});
return result;
这样我们就实现一个相对来说完整的深拷贝!!
最终实现
function _completeDeepClone(obj, visited = new WeakMap()) {
if (typeof obj !== "object" || obj === null) return obj;
// 防止循环引用
if (visited.has(obj)) {
return visited.get(obj);
}
// 获取对象的构造函数
const constructor = obj.constructor;
// 处理特殊对象类型~正则对象和时间对象
if(/^(RegExp|Date|WeakMap|WeakSet)/.test(constructor.name)) {
const res = new constructor(obj);
visited.set(obj, res);
return res;
}
// Map
if (constructor === Map) {
const map = new Map();
visited.set(obj, map);
obj.forEach((value, key) => {
map.set(key, _completeDeepClone(value, visited));
});
return map;
}
// Set
if (constructor === Set) {
const set = new Set();
visited.set(obj, set);
obj.forEach((value) => {
set.add(_completeDeepClone(value, visited));
});
return set;
}
// 处理函数对象和箭头函数
if(constructor === Function) {
let res;
if(!obj.hasOwnProperty("prototype")) { // 箭头函数
res = new Function(`return ${obj.toString()}`)()
visited.set(obj, res); // 避免循环引用
return res;
}
// 考虑到构造函数和普通对象
res = function(...args) {
return obj.call(this, ...args);
}
visited.set(obj, res); // 避免循环引用
// 普通对象的自身的属性,比如fn.a = 2这种静态属性
Object.keys(obj).forEach(key => res[key] = obj[key]);
// 原型继承,寄生组合继承,复制该函数的原型链
res.prototype = Object.create(obj.prototype);
res.prototype.constructor = res;
return res;
}
// 处理普通对象和数组
const result = Array.isArray(obj) ? [] : {};
visited.set(obj, result);
// 获取对象的所有属性名,包括不可枚举属性
const props = Object.getOwnPropertyNames(obj); // 不可枚举类型
const symbolProps = Object.getOwnPropertySymbols(obj); // Symbol类型
props.concat(symbolProps).forEach(key => {
const descriptor = Object.getOwnPropertyDescriptor(obj, key);
if(descriptor) {
const { value, writable, enumerable, configurable } = descriptor;
Object.defineProperty(result, key, {
value: _completeDeepClone(value, visited),
writable, enumerable, configurable
})
}
});
return result;
}
深拷贝业务场景?
不得不说,难免有些sx面试官还会问这个问题,服了u。
- 数据缓存:当需要对某个对象进行数据缓存时,为了避免对象引用带来的问题,可以使用深拷贝来创建一个独立的对象存储数据。
- 数据传递:在多个模块之间传递数据时,为了避免数据被污染,也可以使用深拷贝将数据拷贝到新的对象中,确保数据的独立性。
- 状态保存:在一些需要保存状态的场景中,比如撤销/重做功能、多步操作等,为了保证状态不被覆盖,也可以使用深拷贝来保存状态。
- 对象转换:当需要将一个对象转换为另一个对象时,可以使用深拷贝来创建一个与原对象结构相同的新对象,并将原对象的数据复制到新对象中。
但是深拷贝也会带来内存占用过大问题,在实际业务中尽量不用到。
完结撒花