跳到主要内容

深拷贝

2024年04月09日
柏拉文
越努力,越幸运

一、认识


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

二、object、array


function deepClone(origin) {
if (typeof origin !== "object") {
return origin;
}
const target = Array.isArray(origin) ? [] : {};
for (let key in origin) {
target[key] = deepClone(origin[key]);
}
return target;
}

测试用例

const object = {
id: 3,
like: {
ball: "篮球",
},
love: ["唱歌", "跳舞", "rap"],
};

const objectCopy = deepClone(object);

objectCopy.love[0] = "唱歌修改";

console.log(object);
console.log(objectCopy);

三、object、array、map


通过 Map 来解决对象循环引用拷贝的问题。额外开辟一个存储空间,来存储当前源对象和拷贝新对象的对应关系。当需要拷贝当前源对象时,先去存储空间去找有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝。存储空间选择Map数据结构

function deepClone(origin, map) {
if (typeof origin !== "object") {
return origin;
}
map = map || new Map();
if (map.has(origin)) {
return map.get(origin);
}
const target = Array.isArray(origin) ? [] : {};
map.set(origin, target);
for (let key in origin) {
target[key] = deepClone(origin[key], map);
}
return target;
}

测试用例

const object = {
id: 3,
like: {
ball: "篮球",
},
love: ["唱歌", "跳舞", "rap"],
};
object.object = object;

const objectCopy = deepClone(object);

objectCopy.object.love[0] = "唱歌修改";

console.log(object);
console.log(objectCopy);
console.log(object === objectCopy); // false
console.log(object.object === objectCopy.object); // false

四、object、array、weakMap


通过 WeakMap 来解决对象循环引用拷贝的问题。当拷贝对象非常庞大的时候,如果使用Map,那么对象与Map之间存在强引用的关系,即使对象已经被释放内存,那么Map对象的引用还在,无法释放内存,所以使用Map会对内存造成非常大的额外消耗,是开发者不得不手动清除Map。如果使用WeakMap,那么对象与WeakMap是弱引用的关系,对象被释放内存后,WeakMpa 的内存也会被回收。

const object = {
id: 3,
like: {
ball: "篮球",
},
love: ["唱歌", "跳舞", "rap"],
};
object.object = object;

function deepClone(origin, weakMap = new WeakMap()) {
if (typeof origin === "object") {
if (weakMap.get(origin)) {
return weakMap.get(origin);
}
const target = Array.isArray(origin) ? [] : {};
weakMap.set(origin, target);
for (const key in origin) {
target[key] = deepClone(origin[key], weakMap);
}
return target;
} else {
return origin;
}
}

const objectCopy = deepClone(object);
console.log(objectCopy);

进阶版本-性能优化: 遍历数组和对象使用for in,遍历效率非常低,可以使用while代替for in

function arrayEach(array, iteratee) {
let index = -1;
const { length } = array;
while (++index < length) {
iteratee(array[index], index);
}
return array;
}
function deepClone(origin, weakMap = new WeakMap()) {
if (typeof origin === "object") {
if (weakMap.get(origin)) {
return weakMap.get(origin);
}
const isArray = Array.isArray(origin);
const result = isArray ? [] : {};
weakMap.set(origin, result);
const props = isArray ? undefined : Object.keys(origin);
arrayEach(props || origin, (value, key) => {
if (props) {
key = value;
}
result[key] = deepClone(origin[key], weakMap);
});
return result;
} else {
return origin;
}
}

const object = {
id: 3,
like: {
ball: "篮球",
},
love: ["唱歌", "跳舞", "rap"],
};
object.object = object;
const objectCopy = deepClone(object);
console.log(objectCopy);
console.log(object.like === objectCopy.like);
console.log(object.love === objectCopy.love);

亮点: 如何拷贝 RegExp 对象

const regExp = new RegExp("^[\d]*$","i");
console.dir(regExp);

const regExps = new RegExp(regExp.source,/\w*$/.exec(regExp));
regExps.lastIndex = regExp.lastIndex;
console.dir(regExps)

亮点: 如何拷贝 RegExp.exec 数组

const regExp = /(\d*)/;
const str = "ab2334ddd";
const origin = regExp.exec(str);
let result = [];

if (
origin.length &&
typeof origin[0] === "string" &&
Object.prototype.hasOwnProperty.call(origin, "index")
) {
result.index = origin.index;
result.input = origin.input;
}

亮点: 如何拷贝对象原型

function isPrototype(origin) {
const ctor = origin && origin.constructor;
const proto =
(typeof ctor === "function" && ctor.prototype) || Object.prototype;
return origin === proto;
}
function cloneObject(origin) {
return typeof origin.constructor === "function" && !isPrototype(origin)
? Object.create(origin)
: {};
}

function Person(){

}
Person.prototype.name = "Person";

const obj = new Person();
const objs = cloneObject(obj);
console.log(objs.name)
console.log(Object.getPrototypeOf(objs))

亮点: 如何拷贝 Symbol

const symbol = Symbol('哈哈');
const symbols = Object(Symbol.prototype.valueOf.call(symbol))

亮点: 如何拷贝函数

function cloneFunction(func) {
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{)/;
const funcString = func.toString();
if (func.prototype) {
const body = bodyReg.exec(funcString);
const param = paramReg.exec(funcString);
if (body) {
if (param) {
const paramArray = param[0].split(",");
return new Function(...paramArray, body[0]);
} else {
return new Function(body[0]);
}
} else {
return null;
}
} else {
return eval(funcString);
}
}

最终版本

deepClone.js
const mapTag = "[object Map]";
const setTag = "[object Set]";
const arrayTag = "[object Array]";
const objectTag = "[object Object]";
const argsTag = "[object Arguments]";

const boolTag = "[object Boolean]";
const dateTag = "[object Date]";
const numberTag = "[object Number]";
const regexpTag = "[object RegExp]";
const stringTag = "[object String]";
const symbolTag = "[object Symbol]";
const errorTag = "[object Error]";
const functionTag = "[object Function]";
const deepTag = [mapTag, setTag, arrayTag, objectTag, argsTag];

function isObject(origin) {
const type = typeof origin;
return origin != null && (type === "object" || type === "function");
}
function getTag(origin) {
if (origin == null) {
return origin === undefined ? "[object Undefined]" : "[object Null]";
}
return Object.prototype.toString.call(origin);
}
function initCloneArray(array) {
const { length } = array;
const result = new array.constructor(length);
if (
length &&
typeof array[0] === "string" &&
Object.prototype.hasOwnProperty.call(array, "index")
) {
result.index = array.index;
result.input = array.input;
}
return result;
}
function isPrototypeFun(object) {
const ctor = object && object.constructor;
const proto =
(typeof ctor === "function" && ctor.prototype) || Object.prototype;
return object === proto;
}
function initCloneObject(object) {
const isNotPrototype =
typeof object.constructor === "function" && !isPrototypeFun(object);
if (isNotPrototype) {
return Object.create(Object.getPrototypeOf(object));
}
return {};
}
function getInit(origin, tag) {
switch (tag) {
case arrayTag:
return initCloneArray(origin);
case objectTag:
return initCloneObject(origin);
default:
return new origin.constructor();
}
}
function cloneRegExp(regexp) {
const result = new regexp.constructor(regexp.source, /w*$/.exec(regexp));
result.lastIndex = regexp.lastIndex;
return result;
}
function cloneSymbol(symbol) {
return Object(Symbol.prototype.valueOf.call(symbol));
}
function cloneFunction(func) {
const bodyReg = /(?<={)(.|\n)+(?=})/m;
const paramReg = /(?<=\().+(?=\)\s+{)/;
const funcString = func.toString();
if (func.prototype) {
const body = bodyReg.exec(funcString);
const param = paramReg.exec(funcString);
if (body) {
if (param) {
const paramArray = param[0].split(",");
return new Function(...paramArray, body[0]);
} else {
return new Function(body[0]);
}
} else {
return null;
}
} else {
return eval(funcString);
}
}
function cloneByTag(origin, tag) {
const Ctor = origin.constructor;
switch (tag) {
case boolTag:
case numberTag:
case stringTag:
case errorTag:
case dateTag:
return new Ctor(origin);
case regexpTag:
return cloneRegExp(origin);
case symbolTag:
return cloneSymbol(origin);
case functionTag:
return cloneFunction(origin);
default:
return null;
}
}
function getSymbols(object) {
if (object === null) {
return [];
}
object = Object(object);
return Object.getOwnPropertySymbols(object).filter((symbol) => {
Object.prototype.propertyIsEnumerable.call(object, symbol);
});
}
function getAllProps(object) {
const result = Object.keys(object);
if (!Array.isArray(object)) {
result.push(...getSymbols(object));
}
return result;
}
function arrayEach(array, iteratee) {
let index = -1;
const length = array.length;
while (++index < length) {
iteratee(array[index], index);
}
return array;
}
function deepClone(origin, weakMap = new WeakMap()) {
if (!isObject(origin)) {
return origin;
}
let result;
const tag = getTag(origin);
if (deepTag.includes(tag)) {
result = getInit(origin, tag);
// console.log("origin",origin,result === origin)
} else {
return cloneByTag(origin, tag);
}
if (weakMap.get(origin)) {
return origin;
}
weakMap.set(origin, result);
if (tag === setTag) {
origin.forEach((value) => {
result.add(deepClone(value));
});
return result;
}
if (tag === mapTag) {
origin.forEach((value, key) => {
result.set(key, deepClone(value));
});
return result;
}
const props = tag === arrayTag ? undefined : getAllProps(origin);
arrayEach(props || origin, (value, key) => {
if (props) {
key = value;
}
result[key] = deepClone(origin[key], weakMap);
});
return result;
}

export default deepClone;
调试
import deepClone from "./deepClone";

const obj = {
id: 1,
age: undefined,
desd: null,
num: new Number(2),
date: new Date(),
error: new Error(),
str: new String("哈哈"),
bool: new Boolean(true),
symbol1: Object(Symbol(1)),
regexp: new RegExp("d+"),
like: ["篮球", "足球"],
name: {
family: "柏",
last: "拉文",
},
reg: /\d+/,
retExec: /([\w])+\.js/.exec("abc.js"),
set: new Set([1, 2, 3]),
map: new Map([
[1, "哈哈"],
[2, "嘻嘻"],
]),
[Symbol("symbol属性")]: "symbol属性",
say: () => {
console.log(this);
console.log("说话");
},
done: function () {
console.log(this);
console.log("做事");
},
};
obj.obj = obj;
Object.setPrototypeOf(obj, { a: 1, b: 2 });
const objCopy = deepClone(obj);


objCopy.name.last = "拉文修改";
console.log(objCopy);
console.log(obj.bool === objCopy.bool);
console.log(obj.date === objCopy.date);
console.log(obj.error === objCopy.error);
console.log(obj.like === objCopy.like);
console.log(obj.reg === objCopy.reg);
console.log(obj.regexp === objCopy.regexp);
console.log(obj.regExec === objCopy.regExec);
console.log(obj.set === objCopy.set);
console.log(obj.map === objCopy.map);
console.log(obj.say === objCopy.say);
console.log(obj.done === objCopy.done);
objCopy.say();
objCopy.done();

方案二、JSON.parse(JSON.stringify())

语法

 const object = {
id: 3,
like: {
ball: "篮球",
},
};
const objectCopy = JSON.parse(JSON.stringify(object));
console.log(objectCopy);

特点

JSON.parse(JSON.stringify()) 可以应对大部分的应用场景,但是无法拷贝其他引用类型、拷贝函数、循环引用

参考资料


如何写出一个惊艳面试官的深拷贝?