小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
前言
我们上上篇已经讲了两种深复制对象的方法,少量代码实现了功能,各有优缺点。 上篇通过使用遍历而不是递归,解决了爆栈问题,
今天我们继续探讨循环引用
大致的思路
- 保存所有遍历过值类型属于引用类型的值
- 赋值前,检查当前被拷贝的数据是不是自身有此属性,如果有,不进行复制
- 克隆完毕,清理
复杂的问题,都一定可以被分解,变成相对简单的步骤!!!
实现
和上一篇遍历的代码基本相似,就是多了:
- 初始化WeakMap
- 存值和比对值
- 清理
下面的代码修改自:github.com/jsmini/clon…
去掉了多余的代码和对第三方库的依赖,可以直接复制走,直接用。
const { toString, hasOwnProperty } = Object.prototype;
function hasOwnProp(obj, property) {
return hasOwnProperty.call(obj, property)
}
function getType(obj) {
return toString.call(obj).slice(8, -1).toLowerCase();
}
function isObject(obj) {
return getType(obj) === "object";
}
function isArray(arr) {
return getType(arr) === "array";
}
function isCloneObject(obj) {
return isObject(obj) || isArray(obj)
}
function cloneDeep(x) {
let uniqueData = new WeakMap();
let root = x;
if (isArray(x)) {
root = [];
} else if (isObject(x)) {
root = {};
}
// 循环数组
const loopList = [
{
parent: root,
key: undefined,
data: x,
}
];
while (loopList.length) {
// 深度优先
const node = loopList.pop();
const parent = node.parent;
const key = node.key;
const source = node.data;
// 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
let target = parent;
if (typeof key !== 'undefined') {
target = parent[key] = isArray(source) ? [] : {};
}
// 复杂数据需要缓存操作
if (isCloneObject(source)) {
// 命中缓存,直接返回缓存数据
let uniqueTarget = uniqueData.get(source);
if (uniqueTarget) {
parent[key] = uniqueTarget;
continue; // 中断本次循环
}
// 未命中缓存,保存到缓存
uniqueData.set(source, target);
}
if (isArray(source)) {
for (let i = 0; i < source.length; i++) {
if (isCloneObject(source[i])) {
// 下一次循环
loopList.push({
parent: target,
key: i,
data: source[i],
});
} else {
target[i] = source[i];
}
}
} else if (isObject(source)) {
for (let k in source) {
if (hasOwnProp(source, k)) {
if (isCloneObject(source[k])) {
// 下一次循环
loopList.push({
parent: target,
key: k,
data: source[k],
});
} else {
target[k] = source[k];
}
}
}
}
}
uniqueData = null;
return root;
}
测试循环引用
那我们一起看看测试结果吧。
测试数据
var a = {
p1: "p1",
p2: ["p22", {
p23: a,
p24: 666
}],
p3: a
}
测试结果
最后输出如下,对值为a的属性都被无视了,666啊。
{
"p1": "p1",
"p2": [
"p22",
{
"p24": 666
}
]
}
性能
本想借用一张深拷贝的终极探索(99%的人都不知道)的图 , 但这样显得不负责,那么,我们一起来吧。
我们就借用其生产测试数据的代码:
function createData(deep, breadth) {
var data = {};
var temp = data;
for (var i = 0; i < deep; i++) {
temp = temp['data'] = {};
for (var j = 0; j < breadth; j++) {
temp[j] = j;
}
}
return data;
}
计时代码:
function runWithTimes(times, tag, fn, ...args) {
var stime = performance.now();
for (let i = 0; i < times; i++) {
fn(...args)
}
console.log(` ${tag} cost:`, performance.now() - stime);
}
测试代码:
const data100_100 = createData(100, 100);
const data1000_1000 = createData(1000, 1000);
runWithTimes(100, 'clone', clone, data100_100)
runWithTimes(100, 'clone', clone, data1000_1000)
console.log("--------------------------------")
runWithTimes(100, 'cloneJSON', cloneJSON, data100_100)
runWithTimes(100, 'cloneJSON', cloneJSON, data1000_1000)
console.log("--------------------------------")
runWithTimes(100, 'cloneLoop', cloneLoop, data100_100)
runWithTimes(100, 'cloneLoop', cloneLoop, data1000_1000)
console.log("--------------------------------")
runWithTimes(100, 'cloneCycle', cloneCycle, data100_100)
runWithTimes(100, 'cloneCycle', cloneCycle, data1000_1000)
结果:
clone cost: 101.67948794364929
clone cost: 7295.214939117432
--------------------------------
cloneJSON cost: 156.07540082931519
cloneJSON cost: 18811.49766278267
--------------------------------
cloneLoop cost: 183.07996702194214
cloneLoop cost: 15611.142045974731
--------------------------------
cloneCycle cost: 170.82444596290588
cloneCycle cost: 14335.523449897766
- 朴实无华,就是最快。
- 其余的,深度和广度增加的时候,差别反而不是太大。
- 深度广度增加, JSON方式性能最差
更对对比,参见:深拷贝的终极探索(99%的人都不知道)
小结
到此为止,我们数据的深拷贝相关的内容已经结束。 深拷贝,看起来容易,实际也没那么容易,最主要的是要分你的使用场景,如果是纯数据, 需要这么麻烦吗?
当然不需要,合适的场景选择合适的方法,不香吗?
今天你收获了吗?