Skip to main content

深浅拷贝

深浅拷贝的定义

浅拷贝

创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址指向的值,就会影响到另一个对象。(创建一个新的对象,遍历对象的属性,直接target[key] = origin[key],一遍遍历完成则拷贝结束)

深拷贝

将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象

浅拷贝的方式

  • target = Object.assign({}, origin)

  • 扩展运算符

  • 数组:concat、slice

  • 自定义函数

function clone(origin) {
const target = {};
for (let key in origin) {
if (origin.hasOwnProperty(key)) {
target[key] = origin[key];
}
}
return target;
}

深拷贝的方式

  • JSON.parse(JSON.stringify(obj))

但是这样的拷贝方式有很多缺陷:

JSON.stringify 的一些特点 🙋

  • 对于属性值为 undefined、symbol、函数的属性会被过滤,如果这些类型的值作为数组的元素,则会被转换为 null;函数或者 undefined 单独被转换时,会直接返回 undefined,JSON.stringify(function() {}) -> undefined/JSON.stringify(undefined) -> undefined

  • 其他类型的对象,包括 Map/Set/WeakMap/WeakSet,仅会序列化可枚举的属性。一般来说都是{}

  • 循环引用会报错

  • bigint 的值也不能被序列化

  • 被转换的对象如果定义了 toJSON 方法,那么会返回调用该方法的返回值

  • 布尔值,数字,字符串的包装对象,在被序列化的过程中会被转换为原始值

  • 所有 symbol 属性的键在转换的时候都会被忽略,即使通过 replacer 函数强制指定包含了他们

  • Date 的日期对象会被转换为字符串

  • NaN,Infinity 和 null 都会被转为 null

  • 不可枚举的属性会被默认忽略

  • 简单版本 但是这个无法解决循环引用的问题

function clone(target) {
if (target && typeof target === "object") {
let cloneTarget = Array.isArray(target) ? [] : {};
for (const key in target) {
cloneTarget[key] = clone(target[key]);
}
return cloneTarget;
} else {
return target;
}
}

解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。

  • 解决循环引用的版本

这个存储空间,需要可以存储 key-value 形式的数据,且 key 可以是一个引用类型,我们可以选择 Map 这种数据结构:

  • 检查 map 中有无克隆过的对象

  • 有 - 直接返回

  • 没有 - 将当前对象作为 key,克隆对象作为 value 进行存储

  • 继续克隆

function clone(target, map = new Map()) {
if (target && 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

WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。

什么是弱引用呢?

在计算机程序设计中,弱引用与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。

举个例子:

如果我们使用 Map 的话,那么对象间是存在强引用关系的:

let obj = { name: "Lily" };
const target = new Map();
map.set(obj, "123");
obj = null;

虽然我们手动将 obj,进行释放,然是 target 依然对 obj 存在强引用关系,所以这部分内存依然无法被释放。

再来看 WeakMap:

let obj = { name: "Lily" };
const target = new WeakMap();
map.set(obj, "123");
obj = null;

如果是 WeakMap 的话, target 和 obj 存在的就是弱引用关系,当下一次垃圾回收机制执行时,这块内存就会被释放掉。

  • 完善版本
function isObject(obj) {
return obj !== null && typeof obj === "object";
}

function deepClone(obj, map = new WeakMap()) {
if (obj === null) return obj;
if (!isObject(obj)) return obj;
if (obj instanceof Date) return new Date(obj);
if (obj instanceof RegExp) return new RegExp(obj);
if (map.has(obj)) return map.get(obj);
const target = new obj.constructor();
map.set(obj, target);
if (obj instanceof Set) {
obj.forEach(val => target.add(deepClone(val, map)));
return target;
}
if (obj instanceof Map) {
map.forEach((val, key) => target.set(key, deepClone(val, map)));
return target;
}
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
target[key] = deepClone(obj[key], map);
}
}
return target;
}

重中之重:浏览器提供了原生的深拷贝 API

原生的深拷贝 API:structuredClone。

它也有些缺点:

  • 原型:无法拷贝对象的原型链。
  • 函数:无法拷贝函数。
  • 不可克隆:并没有支持所有类型的拷贝,比如 Error。

但是对于我们平常使用的拷贝功能是够用了