JavaScript对象深拷贝/浅拷贝碰到的坑和解决方法

假如本文对您有任何协助或许您有任何想要提出的看法或题目,请在本文下方复兴,真挚迎接列位介入议论,望列位不吝珠玉。

原载本身的小博客
JavaScript对象拷贝碰到的坑和处理要领 | 手柄君的小阁,所以无耻地算原创吧

近期介入某集训,JavaScript,碰到一对象拷贝题目,取得需求:
给一个对象,请编写一个函数,使其能够拷贝一个对象,返回这个拷贝取得的新对象:
举例以下:

function clone(obj){
    //DO SOMETHING
    return newObject; //返回拷贝取得的新对象
}

起首想到解法以下:

> ES6解构赋值(浅拷贝):

function clone(obj){
    return {...obj};
}

取得新对象为原始对象浅拷贝,即属性Key一致,值假如是数或许字符串则值通报,否则为地点通报,即Value援用和源对象一致,可依据下方运转测试:

var a = {a:1, b:2, c:3, d:[0, 1, 2]}
var b = clone(a);
console.log(b.d[1]); //1
b.d[1] = 2;
console.log(b.d[1]); //2
console.log(a.d[1]); //2

对复制后的对象中包含的数组或许对象举行编辑,影响了源对象,这明显不是我们想要的结果,然则在对象内不包含数组或对象时,该要领不失为一个疾速建立对象拷贝的有用要领。
在ES6中,Object供应了一个 assign() 要领,也能够完成雷同结果

> ES6 Object.assign()(浅拷贝):

function clone(obj){
    return Object.assign({},obj);
}

运转结果和前一种体式格局基础一致,依据MDN形貌,Object.assign() 要领用于将一切可罗列属性的值从一个或多个源对象复制到目的对象,许可最少两个参数,第一个参数为拷贝的目的对象,在要领实行完毕后会被返回,其他参数将作为拷贝泉源。
前面两种要领均为浅拷贝,那末关于对象内包含对象或数组的对象,我们该如何拷贝呢?
我们的先生供应了一种要领以下,缺点稍后再谈

> For…in遍历并递归(深拷贝):

function clone(obj) {
    var newobj = obj.constructor === Array ? [] : {};
    if (typeof obj !== "object") {
        return obj;
    } else {
        for (var i in obj) {
            newobj[i] = typeof obj[i] === "object" ? clone(obj[i]) : obj[i];
        }
    }
    return newobj;
}

一样运用前文中的测试数据:

var a = {a:1, b:2, c:3, d:[0, 1, 2]}
var b = clone(a);
console.log(b.d[1]); //1
b.d[1] = 2;
console.log(b.d[1]); //2
console.log(a.d[1]); //1

可见该要领能够准确地对对象举行深拷贝,并依据参数范例为数组或对象举行举行推断并离别处置惩罚,然则该要领有肯定缺点:

1,在存在Symbol范例属性key时,没法准确拷贝,能够尝试以下测试数据:

var sym = Symbol();
var a = {a:1, b:2, c:3, d:[0, 1, 2], [sym]:"symValue"}
var b = clone(a);
b.d[1] = 2;
console.log(b.d[1]); //2
console.log(a.d[1]); //1
console.log(a[sym]); //"symValue"
console.log(b[sym]); //undefined

能够发明拷贝取得的对象b,不存在Symbol范例对象为属性名的属性。
那末能够发明,题目重要出在For…in遍历属性没法取得Symbol范例Key致使,那末有什么要领能够遍历到这些呢?
在ES6中Reflect包含的静态要领ownKeys() 能够获取到这些key,依据MDN形貌,这个要领获取到的返回值等同于Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))。

那末运用ES6解构赋值和Reflect.ownKeys() 组合运用,改写上文函数,取得:

> ES6解构赋值 & Reflect.ownKeys() 遍历并递归(深拷贝):

function clone(obj) {
    var newobj = obj.constructor === Array ? [...obj] : {...obj};
    if (typeof obj !== "object") {
        return obj;
    } else {
        Reflect.ownKeys(newobj).forEach(i => {
            newobj[i] = typeof obj[i] === "object" ? clone(obj[i]) : obj[i];
        });
    }
    return newobj;
}

运转雷同的测试语句:

var sym = Symbol();
var a = {a:1, b:2, c:3, d:[0, 1, 2], [sym]:"symValue"}
var b = clone(a);
b.d[1] = 2;
console.log(b.d[1]); //2
console.log(a.d[1]); //1
console.log(a[sym]); //"symValue"
console.log(b[sym]); //"symValue"
b[sym] = "newValue";
console.log(a[sym]); //"symValue"
console.log(b[sym]); //"newValue"

能够发明Symbol范例的key也被准确拷贝并赋值了,然则该要领依旧有肯定题目,以下:

2,在对象内部存在环时,客栈溢出,尝试运转以下测试语句:

var a = { info: "a", arr: [0, 1, 2] };
var b = { data: a, info: "b", arr: [3, 4, 5] };
a.data = b;
var c = clone(a); //Error: Maximum call stack size exceeded. 报错:客栈溢出

处理这个的要领稍后再讲,但现在来看已有的两种深拷贝要领充足日常平凡运用,接下来恰好提一下,ES5.1中包含的JSON对象,运用该对象亦可对对象举行深拷贝,会碰到的题目和第一种深拷贝体式格局一样,没法纪录Symbol为属性名的属性,别的只能包含能用JSON字符串示意的数据范例,完成代码以下:

> JSON对象转义(深拷贝):

function clone(obj) {
    return JSON.parse(JSON.stringify(obj);
}

JSON.stringify() 起首将对象序列化为字符串,再由JSON.parse() 反序列化为对象,构成新的对象。
回到前面提到的题目2,假如对象内包含环,怎么办,我的完成思绪为运用两个对象作为相似HashMap,纪录源对象的构造,并在每层遍历前搜检对象是不是已被拷贝过,假如是则从新指向到拷贝好的对象,防备无穷递归。完成代码以下(配有解释):

> Map纪录并递归(深拷贝)

/**
 * 深拷贝(包含Symbol)
 * @param {Object} obj
 */
function clone(obj) {
    const map = {}; //空对象,纪录源对象
    const mapCopy = {}; //空对象,纪录拷贝对象
    /**
     * 在theThis对象中,查找e对象的key,假如找不到,返回false
     * @param {Object} e 要查找的对象
     * @param {Object} theThis 在该对象内查找
     * @returns {symbol | boolean}
     */
    function indexOfFun(e, theThis) {
        let re = false;
        for (const key of Reflect.ownKeys(theThis)) {
            if (e === theThis[key]) {
                re = key;
                break;
            }
        }
        return re;
    }
    /**
     * 在Map对象中,查找e对象的key
     * @param {Object} e 
     */
    const indexOfMap = e => indexOfFun(e, map);
    /**
     * 在Map中纪录obj对象内一切对象的地点
     * @param {Object} obj 要被纪录的对象
     */
    function bindMap(obj) {
        map[Symbol()] = obj;
        Reflect.ownKeys(obj).forEach(key => {
            //当属性范例为Object且还没被纪录过
            if (typeof obj[key] === "object" && !indexOfMap(obj[key])) {
                bindMap(obj[key]); //纪录这个对象
            }
        });
    }
    bindMap(obj);
    /**
     * 拷贝对象
     * @param {Object} obj 要被拷贝的对象
     */
    function copyObj(obj) {
        let re;//用作返回
        if (Array.isArray(obj)) {
            re = [...obj]; //当obj为数组
        } else {
            re = { ...obj }; //当obj为对象
        }
        mapCopy[indexOfMap(obj)] = re; //纪录新对象的地点
        Reflect.ownKeys(re).forEach(key => { //遍历新对象属性
            if (typeof re[key] === "object") { //当属性范例为Object
                if (mapCopy[indexOfMap(re[key])]) { //当属性已被拷贝过
                    re[key] = mapCopy[indexOfMap(re[key])]; //修正属性指向到先前拷贝好的对象
                } else {//当属性还没有被拷贝
                    re[key] = copyObj(re[key]); //拷贝这个对象,并将属性指向新对象
                }
            }
        });
        return re; //返回拷贝的新对象
    }
    return copyObj(obj); //实行拷贝并返回
}

运转前面的测试语句:

var a = { info: "a", arr: [0, 1, 2] };
var b = { data: a, info: "b", arr: [3, 4, 5] };
a.data = b;

var c = clone(a);
c.info = "c";
c.data.info = "d";
console.log(a.info); //"a"
console.log(a.data.info); //"b"
console.log(c.info); //"c"
console.log(c.data.info); //"d"

取得该函数能够准确地拷贝带环对象。

在以上议论和研讨完毕后,同砚向我引荐了一个库 lodash,测试了一下该库存在 _.cloneDeep() 要领,完成深拷贝更加完全和细腻,前文题目均没有在该要领内被发明,在这里提一波。

假如本文对您有任何协助或许您有任何想要提出的看法或题目,请在本文下方复兴,真挚迎接列位介入议论,望列位不吝珠玉。
本文原载于https://www.bysb.net/3113.html

点赞

发表评论

电子邮件地址不会被公开。 必填项已用*标注