我记得在一次面试中,面试官向我提出了深拷贝的问题。起初,我列举了几种深拷贝的方法,包括使用lodash
库、JSON.stringify(JSON.parse(obj))
以及原生API structuredClone()
。
随后,面试官继续追问我是否手写过深拷贝函数,我回答说确实有编写过。然后面试官进一步探询,问我在实现深拷贝时考虑了哪些问题。我的回答只包括了初始化一个空数组来处理数组类型,但我未能提到其他关键问题,如引用循环、函数和非枚举属性的考虑。
最终,因为我考虑问题不全面,我失去了这个工作机会。
1、简易版深拷贝
js
// 判断是否是对象的工具函数
function isObject(val) {
const type = typeof val;
return (type === "object" || type === "function") && type !== "null";
}
// 简易深拷贝
function deepClone(obj) {
// 简单数据类型直接返回
if (!isObject(obj)) {
return obj;
}
let newObject = {};
// 如果是数组类型就初始化[]
if (obj instanceof Array) {
newObject = [];
}
for (const key in obj) {
newObject[key] = deepClone(obj[key]);
}
return newObject;
}
2、特殊类型处理
Function
问题:由于函数是用来调用的,没有做深拷贝的必要,对函数做深拷贝反而会浪费内存空间。
解决方法:和简单数据类型做一样的处理即可。
js
if (typeof obj === 'function') {
return obj
}
Set
问题:我们都知道set类型是不可枚举的,所以我们无法被for...in...遍历。
解决方法:可以通过for...of...的方式进行遍历。
js
if (obj instanceof Set) {
const newSet = new Set();
for (const item of obj) {
newSet.add(item);
}
return newSet;
}
Symbol
问题:symbol类型可以选择不拷贝,因为 Symbol 是一种原始数据类型,每个 Symbol 都是唯一的,不具备可比较性。通常情况下,深拷贝的目的是复制对象的结构和值,而 Symbol 的唯一性和不可比较性使得它在深拷贝时可能不是一个有意义的操作,但是如果仍然希望在深拷贝时处理 Symbol 类型。
解决方法:可以重新创建一个symbol
js
if (typeof obj === "symbol") {
const newSymbol = Symbol(obj.description);
return newSymbol;
}
当Symbol作为key时
为什么我们要考虑symbol作为key时的情况?下面举个例子
问题:
js
const symbol1 = Symbol(1);
const symbol2 = Symbol(2);
const obj = {
name: "symbol",
[symbol1]: "symbol1",
[symbol2]: "symbol2",
};
for (const key in obj) {
console.log(key); // 只会输出name
}
我们会发现当Symbol类型作为key时被忽略了,这是因为Symbol属性默认是不可枚举的
解决方法 :可以使用 Object.getOwnPropertySymbols()
获取,单独遍历Symbol
Object.getOwnPropertySymbols()
返回一个包含给定对象所有自有Symbol
属性的数组。 如[ Symbol(1), Symbol(2) ]
js
const symbolKeys = Object.getOwnPropertySymbols(obj);
for (const symbolKey of symbolKeys) {
newObject[Symbol(symbolKey.description)] = deepClone(obj[symbolKey], map);
}
3、循环引用处理
问题::循环引用是指一个对象包含对自身或包含对其他对象的引用链,形成一个循环,例如:
js
const obj = {};
obj.a = obj;
如果没有处理循环引用,深拷贝可能会陷入无限循环,因为它会不断地尝试复制对象的属性,其中一个属性是对自身的引用,这将导致无限递归的拷贝过程。这不仅浪费了计算资源,还可能导致程序崩溃。
解决方法 :在深拷贝过程中使用一个数据结构来存储已经拷贝过的对象。你可以选择使用Map
或Set
来实现这个数据结构,其中键是原始对象,值是拷贝后的对象。
我这边使用weakMap
进行存储,因为weakMap
的键是弱引用,这意味着当原始对象被垃圾回收时,与之相关联的缓存数据也会被自动释放。这可以避免内存泄漏问题,因为不再需要的缓存数据会自动清除,而不会导致缓存对象一直占用内存。
js
// 为了共享map,将Map对象作为参数传递进来,
// 可以确保它在不同的递归调用之间是共享的。这意味着在整个深拷贝过程中,所有递归调用都使用相同的缓存对象
function deepClone(obj, map = new WeakMap()) {
if (!isObject(obj)) {
return obj;
}
// 如果存在循环引用,就返回之前所保存的引用
if (map.has(obj)) {
return map.get(obj);
}
let newObject = {};
// 保存引用
map.set(obj, newObject);
if (obj instanceof Array) {
newObject = [];
}
for (const key in obj) {
newObject[key] = deepClone(obj[key], map);
}
return newObject;
}
4、完整版深拷贝
js
// 判断是否是对象的工具函数
function isObject(value) {
const valueType = typeof value;
return (
valueType !== null && (valueType === "object" || valueType === "function")
);
}
function deepClone(obj, map = new WeakMap()) {
// 0.[其他类型处理]如果是symbol,应该重新new一个
if (typeof obj === "symbol") {
const newSymbol = Symbol(obj.description);
return newSymbol;
}
// 1.[其他类型处理]如果是原始数据类型就返回
if (!isObject(obj)) {
return obj;
}
// 2.[其他类型处理]如果是function,就直接返回(函数没必要深拷贝)
if (typeof obj === "function") {
return obj;
}
// 3.[其他类型处理]如果是set,因为set无法被for..in..遍历
if (obj instanceof Set) {
const newSet = new Set();
for (const setItem of obj) {
newSet.add(setItem);
}
return newSet;
}
// [循环引用处理]如果存在循环引用,就返回之前所保存的引用
if (map.get(obj)) {
return map.get(obj);
}
// 4.[其他类型处理]如果是数组初始数据就用[], 对象就用{}
const newObject = obj instanceof Array ? [] : {};
// [循环引用处理]保存引用(解决循环引用问题)
map.set(obj, newObject);
// 5.1.遍历普通的key
for (const key in obj) {
newObject[key] = deepClone(obj[key], map);
}
> > > > // 5.2.[其他类型处理]当symbol作为key时会被忽略,所以单独遍历symbol
const symbolKeys = Object.getOwnPropertySymbols(obj);
for (const symbolKey of symbolKeys) {
newObject[Symbol(symbolKey.description)] = deepClone(obj[symbolKey], map);
}
return newObject;
}
5.总结
总之,通过实现深拷贝,我们能够深入研究数据类型和它们的特性,以及优化程序性能。尽管在实际开发中我们可能能够依赖现有的深拷贝工具,但通过亲自实现深拷贝,我们能够更全面地掌握相关概念和技术。