浅拷贝
浅拷贝只新建拷贝对象的第一层,剩下的不变。
const obj = {
a: 1
b: {
c: 2
}
}
const clonedObj = shallowClone(obj);
console.log(obj === clonedObj) // false
console.log(obj['b'] === clonedObj['b']) // true
Lodash的clone方法
比较灵活,支持多种类型的拷贝
const _ = require('lodash');
const obj = {
a: 1,
b: {
c: 0
},
set: new Set('e')
}
const clonedObj = _.clone(obj);
console.log(clonedObj);
console.log(obj.set === clonedObj.set); // true
const _ = require('lodash');
const clonedObj = _.clone({'a': 1});
console.log(clonedObj)
Object.assign
Object.assign() 将一个或者多个源对象中所有可枚举的自有属性复制到目标对象,并返回修改后的目标对象。
const shallowClone = (obj) => {
return Object.assign({}, obj);
}
const clonedObj = shallowClone({'a': 1});
console.log(clonedObj);
使用起来便捷但是要注意使用方式:
- 原型链上的属性和不可枚举的属性不能被复制
- 参考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
ES6的展开运算符
const shallowClone = (obj) => {
return { ...obj };
}
const clonedObj = shallowClone({'a': 1});
console.log(clonedObj)
Array.prototype.xxx(适用于数组)
- Array.prototype.slice
const arr = [1, 2, 3];
const clonedObj = arr.slice(0, arr.length);
console.log(clonedObj);
- Array.prototype.concat
const arr = [1, 2, 3];
const clonedObj = [].concat(arr);
console.log(clonedObj);
深拷贝
深拷贝新建拷贝对象的每一层。
const obj = {
a: 1
b: {
c: 2
}
}
const clonedObj = deepClone(obj);
console.log(obj === clonedObj) // false
console.log(obj['b'] === clonedObj['b']) // false
Lodash库的cloneDeep方法
比较灵活,支持多种类型的拷贝
const _ = require('lodash');
const obj = {
a: 1,
b: {
c: 0
},
set: new Set('e')
}
const clonedObj = _.cloneDeep(obj);
console.log(clonedObj);
console.log(obj.set === clonedObj.set); // false
使用递归手动实现
实现起来和lodash的cloneDeep方法类似,不过是简单的阉割版
const isShallowClone = (obj) => {
if (typeof obj === "function") {
// function type
return true;
} else if (typeof obj !== "object" || obj === undefined) {
// basic type
return true;
} else if (obj === null) {
// special object type
return true;
}
return false;
};
const deepClone = (obj) => {
if (isShallowClone(obj)) {
return obj;
}
let clonedObj = undefined;
if (obj instanceof Date) {
return new Date(obj.getTime());
} else if (obj instanceof RegExp) {
return new RegExp(obj.source, obj.flags);
} else if (obj instanceof Map) {
clonedObj = new Map();
for (const [k, v] of obj) {
clonedObj.set(k, deepClone(v));
}
return clonedObj;
} else if (obj instanceof Set) {
const clonedObj = new Set();
for (const v of obj) {
clonedObj.add(deepClone(v));
}
return clonedObj;
} else if (Array.isArray(obj)) {
clonedObj = [];
} else {
clonedObj = {};
}
// iterate own properties
for (const k of Reflect.ownKeys(obj)) {
clonedObj[k] = deepClone(obj[k]);
}
return clonedObj;
};
用例:
const obj1 = {
// =========== 1.基础数据类型 ===========
num: 0, // number
str: "", // string
bool: true, // boolean
unf: undefined, // undefined
nul: null, // null
sym: Symbol("sym"), // symbol
bign: BigInt(1n), // bigint
// =========== 2.Object类型 ===========
// 普通对象
obj: {
name: "我是一个对象",
id: 1,
},
// 数组
arr: [0, 1, { arr2: [-1] }],
// 函数
func: function () {
console.log("我是一个函数");
},
// // 日期
date: new Date(0),
// // 正则
reg: new RegExp("/我是一个正则/ig"),
// // Map
map: new Map().set("mapKey", 1),
map2: new Map().set(
"k1",
new Map().set("k2", [
1,
2,
[3, 4],
() => {
console.log("good");
},
]),
// // Set
set: new Set().add("set"),
// // =========== 3.其他 =====x======
[Symbol("1")]: { [Symbol("ok")]: "nice" }, // Symbol作为key
};
const res = deepClone(obj1);
console.log(res);
上面的代码不能解决循环引用问题(会导致栈溢出)
const a = {name: "a"};
const b = {name: "b"};
a.b = b;
b.a = a; // 相互引用,代码会不断从a -> b, b-> a,导致不断消耗栈空间最终栈溢出
const res = deepClone(a);
console.log(res);
RangeError: Maximum call stack size exceeded
at deepClone
at deepClone
at deepClone
at deepClone
.....
使用标记法解决循环依赖:
const deepClone = (obj) => {
const isShallowClone = (obj) => {
if (typeof obj === "function") {
// function type
return true;
} else if (typeof obj !== "object" || obj === undefined) {
// basic type(number, string, boolean...)
return true;
} else if (obj === null) {
// special object type
return true;
}
return false;
};
// 1. 使用(新-旧)键值来判断发生并解决循环引用导致的stackoverflow问题
// 2. 使用WeakMap避免闭包导致的内存泄漏
const cache = new WeakMap();
const _deepClone = (obj) => {
if (isShallowClone(obj)) {
return obj;
}
if (cache.get(obj)) {
return cache.get(obj);
}
let clonedObj = undefined;
if (obj instanceof Date) {
return new Date(obj.getTime());
} else if (obj instanceof RegExp) {
return new RegExp(obj.source, obj.flags);
} else if (obj instanceof Map) {
clonedObj = new Map();
for (const [k, v] of obj) {
cache.set(obj, clonedObj);
clonedObj.set(k, _deepClone(v));
}
return clonedObj;
} else if (obj instanceof Set) {
const clonedObj = new Set();
for (const v of obj) {
cache.set(obj, clonedObj);
clonedObj.add(_deepClone(v));
}
return clonedObj;
} else if (Array.isArray(obj)) {
clonedObj = [];
} else {
clonedObj = {};
}
// iterate own properties
for (const k of Reflect.ownKeys(obj)) {
cache.set(obj, clonedObj);
clonedObj[k] = _deepClone(obj[k]);
}
return clonedObj;
};
return _deepClone(obj);
};
用例:
const obj = {
// =========== 1.基础数据类型 ===========
num: 0, // number
str: "", // string
bool: true, // boolean
unf: undefined, // undefined
nul: null, // null
sym: Symbol("sym"), // symbol
bign: BigInt(1n), // bigint
// =========== 2.Object类型 ===========
// 普通对象
obj: {
name: "我是一个对象",
id: 1,
},
// 数组
arr: [0, 1, 2],
// 函数
func: function () {
console.log("我是一个函数");
},
// 日期
date: new Date(0),
// 正则
reg: new RegExp("/我是一个正则/ig"),
// Map
map: new Map().set("mapKey", 1),
// Set
set: new Set().add("set"),
// =========== 3.其他 ===========
[Symbol("1")]: 1, // Symbol作为key
};
obj["loop"] = obj;
const res = deepClone(obj);
console.log(res);
这种递归代码扩展性比较好
- 像Promise这类的类型就可以自定义拷贝方式
- 对于是否拷贝原型链上的属性以及是否继承原型链都可以定制
message channel
message channel一般用于ifram窗口的通信。
这种方法遇到function没法拷贝,会报错
const obj = {
a: 1,
b: 2,
f: () => {}
}
const deepClone_messagechannel = (obj) => {
return new Promise(( resolve ) => {
const { port1, port2 } = new MessageChannel();
port2.onmessage = (obj) => {
resolve(obj.data);
};
port1.postMessage(obj);
});
}
const res = await deepClone_messagechannel(obj);
console.log(res)
Uncaught DOMException: Failed to execute 'postMessage' on 'MessagePort': () => {} could not be cloned.
序列化
function类型以及其他某些类型也不能拷贝
deepClone_stringfy = (obj) => {
const str = JSON.stringify(obj);
return JSON.parse(str);
};
structuredClone方法
结构化克隆算法用于复制复杂 JavaScript 对象的算法。通过来自 Worker 的 postMessage() 或使用 IndexedDB 存储对象时在内部使用。它通过递归输入对象来构建克隆,同时保持先前访问过的引用的映射,以避免无限遍历循环
- Function 对象是不能被结构化克隆算法复制的;如果你尝试这样子去做,这会导致抛出 DATA_CLONE_ERR 的异常。
企图去克隆 DOM 节点同样会抛出 DATA_CLONE_ERR 异常。 - 对象的某些特定参数也不会被保留
- RegExp 对象的 lastIndex 字段不会被保留
- 属性描述符,setters 以及 getters(以及其他类似元数据的功能)同样不会被复制。例如,如果一个对象用属性描述符标记为 read-only,它将会被复制为 read-write,因为这是默认的情况下。
- 原形链上的属性也不会被追踪以及复制。
refer: https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
用例:
const obj = { a: 1 };
const res = structuredClone(a);
console.log(res);
可以看出这种方式还是有很多限制的, 内部也是使用了递归的方式实现的
对比这几种
用例:
const deepNestedObject = {
level1: {
level2: {
level3: {
level4: {
level5: {
level6: {
level7: {
level8: {
level9: {
level10: {
arrayOfNumbers: [1, 2, 3, 4],
anotherObject: {
key1: "value1",
key2: "value2",
},
},
},
},
},
},
},
},
},
deep: [
{
key1: "value1",
key2: "value2",
key3: [
{
nestedKey1: "nestedValue1",
nestedKey2: "nestedValue2",
},
{
nestedKey1: "nestedValue3",
nestedKey2: "nestedValue4",
},
],
},
{
key1: "value3",
key2: "value4",
key3: [
{
nestedKey1: "nestedValue5",
nestedKey2: "nestedValue6",
},
{
nestedKey1: "nestedValue7",
nestedKey2: "nestedValue8",
},
],
},
],
},
},
};
const _ = require("lodash");
const tikTok = (func) => {
const s = new Date().getTime();
for (let i = 0; i < 1000000; i++) {
func();
}
console.log(new Date().getTime() - s);
};
const tikTok2 = async () => {
const s = new Date().getTime();
for (let i = 0; i < 1000000; i++) {
await deepClone_messagechannel(deepNestedObject);
}
console.log(new Date().getTime() - s);
};
tikTok(() => deepClone(deepNestedObject)); // 自定义deepClone
tikTok(() => _.cloneDeep(deepNestedObject)); // lodash的cloneDeep
tikTok(() => deepClone_stringfy(deepNestedObject)); // JSON.stringfy那种序列化形式的
tikTok(() => structuredClone(deepNestedObject)); // structuredClone方法
await tikTok2(); // message channel
在浏览器统一运行输出结果:
6216
8603
5145
11929
50126
速度上:序列化 > 自定义deepClone > lodash > structuredClone > message channel
使用便利性上:lodash、自定义deepClone > structuredClone、序列化 > message channel