对象深拷贝 下

117 阅读1分钟

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

前言

我们上上篇已经讲了两种深复制对象的方法,少量代码实现了功能,各有优缺点。
上篇还自己手写了一个简单版本,也一起品读了jquery.extend方法的实现。

还是那句话哈,正常情况都满足需求,我们不妨再深入一点。

  1. 爆栈
  2. 循环引用

今天解决爆栈

用循环替代递归

采用深度遍历的机制,一个属性一个属性的遍历。

下面的代码修改自: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 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 data = node.data;

        // 初始化赋值目标,key为undefined则拷贝到父元素,否则拷贝到子元素
        let res = parent;
        if (typeof key !== 'undefined') {
            res = parent[key] = isArray(data) ? [] : {};
        }

        if (isArray(data)) {
            for (let i = 0; i < data.length; i++) {
                // 避免一层死循环 a.b = a
                if (data[i] === data) {
                    res[i] = res;
                } else if (isCloneObject(data[i])) { // 需要深度复制的属性值
                    // 下一次循环, 入栈
                    loopList.push({
                        parent: res,
                        key: i,
                        data: data[i],
                    });
                } else {
                    res[i] = data[i];
                }
            }
        } else if (isObject(data)) {
            for (let k in data) {
                if (hasOwnProp(data, k)) {
                    // 避免一层死循环 a.b = a
                    if (data[k] === data) {
                        res[k] = res;
                    } else if (isCloneObject(data[k])) { // 需要深度复制的属性值
                        // 下一次循环
                        loopList.push({
                            parent: res,
                            key: k,
                            data: data[k],
                        });
                    } else {
                        res[k] = data[k];
                    }
                }
            }
        }
    }

    return root;
}

console.log(cloneDeep({ a: 1, b: { fn: function () { } } }))
// { a: 1, b: { fn: [Function: fn] } }

测试爆栈

生产测试数据

生产测试的方法:

function createData(deep) {
    var data = {};
    var temp = data;

    for (var i = 0; i < deep; i++) {
        temp = temp['data'] = {};
        temp[i + 1] = i + 1
    }

    return data;
}

产生的数据结构如下:

{
	"data": {
		"1": 1,
		"data": {
			"2": 2,
			"data": {
				"3": 3
			}
		}
	}
}

JSON.parse(JSON.stringify(data))

const data = createData(10000);
console.log(JSON.parse(JSON.stringify(data)))

结果是爆栈 截图_20210228110256.png

基于循环实现的cloneDeep

const data = createData(10000);
console.log(cloneDeep(data))

果然爆栈不见了,666啊

截图_20210428110408.png

小结

其实也还有用拓展运算符Object.assignArray.prototype.concat结合递归的方式来实现深拷贝方案,有兴趣的同学可以自己去尝试一波。

下一篇,我们一起解决循环引用。

今天你收获了吗?

引用

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