对象深拷贝-循环依赖 终

1,390 阅读1分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

前言

我们上上篇已经讲了两种深复制对象的方法,少量代码实现了功能,各有优缺点。 上篇通过使用遍历而不是递归,解决了爆栈问题,

今天我们继续探讨循环引用

大致的思路

  1. 保存所有遍历过值类型属于引用类型的值
  2. 赋值前,检查当前被拷贝的数据是不是自身有此属性,如果有,不进行复制
  3. 克隆完毕,清理

复杂的问题,都一定可以被分解,变成相对简单的步骤!!!

实现

和上一篇遍历的代码基本相似,就是多了:

  1. 初始化WeakMap
  2. 存值和比对值
  3. 清理

下面的代码修改自: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
  1. 朴实无华,就是最快。
  2. 其余的,深度和广度增加的时候,差别反而不是太大。
  3. 深度广度增加, JSON方式性能最差

更对对比,参见:深拷贝的终极探索(99%的人都不知道)

小结

到此为止,我们数据的深拷贝相关的内容已经结束。 深拷贝,看起来容易,实际也没那么容易,最主要的是要分你的使用场景,如果是纯数据, 需要这么麻烦吗?

当然不需要,合适的场景选择合适的方法,不香吗?

今天你收获了吗?

引用

深拷贝的终极探索(99%的人都不知道)
jsmini