深浅拷贝
浅拷贝:基础数据、对象引用地址,只拷贝第一层 const foo = { ...obj }
深拷贝:拷贝全部,另开空间,不是简单的引用地址
深拷贝
方式
JSON.parse(JSON.stringify(obj))
但是该方法具有以下局限性:
- Date 对象,会将其转化成字符串
- 如果对象存在循环引用,会报错
- 存在 Set、Map、正则、Error 对象,该方法会将其转成空对象字面量 { } ,如果存在 undefined,该方法会直接忽略
- 不能序列化函数
structuredClone
这是个原生的新api,babel有对应的polyfill(core-js@3),可以放心用
- 无限嵌套的对象和数组
- 循环引用
- 如下数据类型:结构化克隆算法 - Web API 接口参考 | MDN
对应的局限性:
- Function 对象是不能被结构化克隆算法复制的;如果你尝试这样子去做,这会导致抛出 DATA_CLONE_ERR 的异常。
- 克隆 DOM 节点同样会抛出 DATA_CLONE_ERR 异常。
- 对象的某些特定参数也不会被复制(getter、setter、原形链上的属性也不会被追踪以及复制)
详情参考:结构化克隆算法 - Web API 接口参考 | MDN
可转移对象
- 简单定义:一些特定的对象可以进行“转移”,转移后原对象将被清空。obj1 -> obj2 后 obj2 会具有 obj1 的内容,而 obj1 将被掏空。
- 详细定义:可转移对象 - Web API 接口参考 | MDN
可以利用 structuredClone 进行“转移”
这东西有什么具体用途吗?这个涉及 worker 的使用
使得 postMessage() API 能够接受不仅是字符串的消息,还接受 File、Blob、ArrayBuffer 和 JSON 对象等复杂类型的消息
lodash.cloneDeep
跟前面二者相比,它更强大,原型链可以继承,getter、setter可以拿下,函数可以被复制...
但是 Lodash 的 tree-shaking 可能和我们的使用心智有一些出入,如果项目脚手架没有处理该问题,引入方式也没有注意,那么会带来一些性能损耗。
解决方案可以参考:www.cnblogs.com/fancyLee/p/…
对比
原型链表现
class Foo {
constructor(name) {
this.name = name;
}
foo() {
console.log("hello", this.name);
}
}
const obj = new Foo("obj");
obj.__proto__.b = {};
const obj2 = lodash.cloneDeep(obj);
const obj3 = structuredClone(obj);
obj2.name = "lodash";
obj3.name = "structuredClone";
console.log(obj2, obj3); // obj2 的原型指向 Foo,obj3 直接指向 Object
console.log(obj.__proto__ === obj2.__proto__); // true
console.log(obj2.__proto__ === obj3.__proto__); // false
手写
丐版
简单做一个递归
const deepClone = (target) => {
if (typeof target !== "object") return target;// 基本数据类型直接返回
const res = Array.isArray(target) ? [] : {}; // 数组和对象的体现形式不同,要区分
for (const key in target) {
res[key] = deepClone(target[key]);
}
return res;
}
但是另外需要关注几个问题
- 循环引用引起的调用栈溢出
- 递归引起的调用栈溢出
- 复杂数据类型
循环引用
如果对象中有个属性是指向对象本身的,即 target.target = target;
那么在深克隆的时候,一旦检测到 target.target 就会复制一个 target ,而这个被复制的对象,内部也需要不断复制,进而陷入死循环然后爆栈。
为了解决循环引用问题,我们可以额外开辟一个存储空间,用来存储已经被复制过的对象地址(基本数据类型直接被返回了),当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。
这个存储空间,需要可以存储 key-value 形式的数据,且 key 可以是一个引用类型,我们可以选择Map这种数据结构:
- 检查
map中有无克隆过的对象 - 有 - 直接返回
- 没有 - 将当前对象作为
key,克隆对象作为value进行存储 - 继续克隆
const deepClone = (target, map = new WeakMap()) => {
if (typeof target !== "object") return target;// 基本数据类型直接返回
if (map.has(target)) return map.get(target); // 解决循环引用问题
const res = Array.isArray(target) ? [] : {}; // 数组和对象的体现形式不同,要区分
map.set(target, res);
for (const key in target) {
res[key] = deepClone(target[key], map); // 记得传入已有的 map
}
return res;
}
递归转遍历
递归层数够深的话,同样也会引起栈溢出的情况
...
const objGenerator = (obj = {}, depth = 6000) => {
let prevObj = obj;
do {
const parentNode = prevObj[`${depth + 1}`] ?? obj;
parentNode[`${depth}`] = {};
prevObj = parentNode;
} while (depth--);
return obj;
};
const obj = objGenerator();
console.log(deepClone(obj)); // 爆了 4300
// console.log(lodash.cloneDeep(obj)); // 也爆了 4200(粗略看了眼源码,内部也是递归的方式)
// console.log(structuredClone(obj)); // 都爆了 1100
解决方案是用遍历的方式
function deepCloneV2(x) {
const uniqueList = []; // 处理循环引用
let root = {};
// 循环数组
const loopList = [
{
parent: root,
key: undefined,
data: x,
},
];
while (loopList.length) {
// 深度优先
const node = loopList.pop();
const { parent, key, data } = node;
// 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
let res = parent;
if (typeof key !== "undefined") {
res = parent[key] = Array.isArray(data) ? [] : {};
}
// 数据已经存在(处理循环引用)
let uniqueData = uniqueList.find((item) => item.source === data);
if (uniqueData) {
parent[key] = uniqueData.target;
continue; // 中断本次循环
}
// 数据未出现过
uniqueList.push({
source: data,
target: res,
});
for (const key in data) {
if (data.hasOwnProperty(key)) {
const value = data[key];
if (typeof value === "object") {
loopList.push({
parent: res,
key,
data: value,
});
// 其他判断可以挂 else...if...
} else {
// 不是对象不用进遍历队列
res[key] = value;
}
}
}
}
return root;
}
复杂数据类型
edge case特别多,不想写了