前端面試題JavaScript- 如何深拷貝一個(gè)對象

在實(shí)際開發(fā)中,我們經(jīng)常會(huì)遇到原對象不能修改,但是又需要用到里面的數(shù)據(jù),比如原對象里面的值是一個(gè)數(shù)組,我們要將他拼接成一個(gè)字符串。
這個(gè)時(shí)候如果可以深拷貝一個(gè)對象,用拷貝出來的這個(gè)對象進(jìn)行操作就會(huì)非常方便,這也是前端面試中,經(jīng)常會(huì)遇到的題,可以考驗(yàn)候選人,對于js的知識(shí)是否掌握的過硬。

對象的拷貝,可以分為深拷貝和淺拷貝。

目前自己總結(jié)的對象拷貝的方法:

  1. 直接賦值 obj2 = obj1
  2. Object對象上的:Object.assign() 方法
  3. JSON 對象上的:JSON.parse(JSON.stringify(obj)) 方法
  4. for in 循環(huán)遍歷對象的每一個(gè)鍵
  5. 更復(fù)雜的自定義方法
  6. 第三方庫:比如 lodash 的cloneDeep

淺拷貝

很多情況我們只要實(shí)現(xiàn)對象的淺拷貝就好了,并不需要真的進(jìn)行深拷貝,但是在進(jìn)行淺拷貝的時(shí)候,一定是經(jīng)過思考的,比如我們就想用里面的簡單數(shù)據(jù)類型而已,不會(huì)對里面的引用值進(jìn)行修改,否則運(yùn)行結(jié)果可能會(huì)在你的意料之外。

一、直接賦值 obj2 = obj1

由于對象的值是存在內(nèi)存的堆中的,直接賦值方法,新的對象指向的是原對象在內(nèi)存中的地址而已,這個(gè)時(shí)候,改變新對象的屬性值,會(huì)通過引用地址去找到內(nèi)存中的真正的值。導(dǎo)致原對象的值也就改變了,這個(gè)直接賦值的方法,真的就是原對象多了個(gè)名字而已,就好像這個(gè)人有個(gè)原名字叫 李雷,你又給他起了個(gè)名字叫 小雷。

二、 Object對象上的:Object.assign() 方法

MDN上這樣介紹Object.assign(),'Object.assign() 方法用于將所有可枚舉的屬性的值從一個(gè)或多個(gè)源對象復(fù)制到目標(biāo)對象。它將返回目標(biāo)對象',好吧,并看不出是深拷貝還是淺拷貝,我們來測試一下

let srcObj = {'name': 'lilei', 'age': '20'};
let copyObj2 = Object.assign({}, srcObj, {'age': '21'});
copyObj2.age = '23';
console.log('srcObj', srcObj); //{ name: 'lilei', age: '22' }
看起來好像是深拷貝了,那其實(shí)這里let copyObj2 = Object.assign({}, srcObj, {'age': '21'}); 我們把srcObj 給了一個(gè)新的空對象。同樣目標(biāo)對象為 {},我們再來測試下:

srcObj = {'name': '明', grade: {'chi': '50', 'eng': '50'} };
copyObj2 = Object.assign({}, srcObj);
copyObj2.name = '紅';
copyObj2.grade.chi = '60';
console.log('新 objec srcObj', srcObj); // { name: '明', grade: { chi: '60', eng: '50' } }
從例子中可以看出,改變復(fù)制對象的name 和 grade.chi ,源對象的name沒有變化,但是grade.chi卻被改變了。因此我們可以看出Object.assign()拷貝的只是屬性值,假如源對象的屬性值是一個(gè)指向?qū)ο蟮囊?,它也只拷貝那個(gè)引用值。
也就是說,對于Object.assign()而言, 如果對象的屬性值為簡單類型(string, number),通過Object.assign({},srcObj);得到的新對象為‘深拷貝’;如果屬性值為對象或其它引用類型,那對于這個(gè)對象而言其實(shí)是淺拷貝的。這是Object.assign()特別值得注意的地方。
多說一句,Object.assign({}, src1, src2); 對于scr1和src2之間相同的屬性是直接覆蓋的,如果屬性值為對象,是不會(huì)對對象之間的屬性進(jìn)行合并的。

三、JSON 對象上的:JSON.parse(JSON.stringify(obj)) 方法

摘選
JSON.parse(JSON.stringify(obj))一般用來實(shí)現(xiàn)深拷貝:
1. JSON.parse()將對象序列化(JSON字符串);
2. JSON.string()實(shí)現(xiàn)反序列化(還原)js對象
復(fù)制代碼
注意一、obj里面有時(shí)間對象,則JSON.stringiify后再JSON.parse的結(jié)果,時(shí)間將只是字符串的形式。而不是時(shí)間對象

    var test = {
        name: 'a',
        date: [new Date(1536627600000), new Date(1540047600000)],
    }
    let b;
    b = JSON.parse(JSON.stringify(test))
    
    1. console.log(b)
        // b = {
        //      name: "a",
        //      data: [
        //          "2018-09-11T01:00:00.000Z",
        //          "2018-10-20T15:00:00.000Z"
        //          ]
        //  }
        (typeof b.date[0])  =>  string
    
    2. console.log(test)  
        // test = {
        //      name: "a",
        //      data: [
        //          "Tue Sep 11 2018 09:00:00 GMT+0800 (中國標(biāo)準(zhǔn)時(shí)間)",
        //          "Sat Oct 20 2018 23:00:00 GMT+0800 (中國標(biāo)準(zhǔn)時(shí)間)",
        //      ]
        // }
        (typeof test.date[0])   =>  object

注意二、如果obj里有RegExp、Error對象,則序列化的結(jié)果將只得到空對象

        name: "a",
        date: new RegExp('\w+')
    }
    const copyed = JSON.parse(JSON.stringify(test));
    test.name = 'test';
    console.error('ddd', test, copyed)
    
    1. console.log(copyed)
        // copyed = {
        //      name: "a",
        //      data: {}
        //  }
    
    2. console.log(test)  
        // test = {
        //      name: "test",
        //      data: new RegExp('\w+')
        // }

注意三、如果obj里有函數(shù),undefined,則序列化的結(jié)果會(huì)把函數(shù)或 undefined丟失;

    const test = {
        name: 'a',
        date: function foo(){
            console.log('haha')
        },
        a: undefined
    }
    const copyed = JSON.parse(JSON.stringify(test))
    
    1. console.log(copyed)
        // copyed = {
        //      name: "a"
        //  }
    
    2. console.log(test)  
        // test = {
        //      name: "test",
        //      date: function foo(){
        //          console.log('haha')
        //      },
        //      a: undefined
        // }

注意四、如果obj里有NaN、Infinity和-Infinity,則序列化的結(jié)果會(huì)變成null

        // test = {
        //      name: "test",
        //      date: null   // 即 NaN返回null
        // }

注意五、JSON.stringify()只能序列化對象的可枚舉的自有屬性,例如 如果obj中的對象是有構(gòu)造函數(shù)生成的,則使用JSON.parse(JSON.stringify(obj))深拷貝后,會(huì)丟棄對象的constructor;

    function Person(name) {
        this.name = name;
        // console.log(name)
    }
    const liai = new Person('liai');
    const test = {
        name: 'a',
        date: liai,
    };
    const copyed = JSON.parse(JSON.stringify(test));
    test.name = 'test'
    console.log(test, copyed)
    
    1. console.log(copyed)
        // copyed = {
        //      name: "a",
        //      data: {name: "liai"}
        //  }
    
    2. console.log(test)  
        // test = {
        //      name: "test",
        //      data: Person{name: "liai"}
        // }

復(fù)制代碼
注意六、如果對象中存在循環(huán)引用的情況也無法正確實(shí)現(xiàn)深拷貝;

四、for in 循環(huán)遍歷對象的每一個(gè)鍵

簡單的for in也是只能實(shí)現(xiàn)對象的一級(jí),簡單數(shù)據(jù)的(字符串 、數(shù)字、布爾值、對空(Null)、未定義(Undefined)、Symbol)淺拷貝,復(fù)雜數(shù)據(jù)比如 值里面存的是一個(gè)對象,function 、array 那就無法深度拷貝。

  • 我們可以用遞歸 + for in 來實(shí)現(xiàn)一個(gè)日常開發(fā)中可以足夠用的淺拷貝
(1)//自定義的復(fù)制方法

function clone(obj) {
var copy = {};
for (var attr in obj) {
copy[attr] = typeof(obj[attr])==='object' ? clone(obj[attr]) : obj[attr];
}
return copy;
}

//測試樣例
var a = {v1:1, v2:2};
var b = clone(a);
b.v1 = 3;
console.log("對象a:",a);
console.log("對象b:",b);

(2)也可以直接給 Object 增加個(gè) clone 方法,其內(nèi)部實(shí)現(xiàn)原來同上面是一樣的。

//自定義的復(fù)制方法
Object.prototype.clone = function() {
var copy = (this instanceof Array) ? [] : {};
for (var attr in this) {
if (this.hasOwnProperty(attr)){
copy[attr] = typeof(this[attr])==='object' ? clone(this[attr]) : this[attr];
}
}
return copy;
};

//測試樣例
var a = {v1:1, v2:2};
var b = a.clone();
b.v1 = 3;
console.log("對象a:",a);
console.log("對象b:",b);

深拷貝

前面四個(gè)方法都無法實(shí)現(xiàn)深拷貝啊,那自己實(shí)現(xiàn)一個(gè)深拷貝真的有那么難嗎?

五、 更復(fù)雜的自定義方法
function  deepClone(data) {      
        const type = this.judgeType(data);      
        let obj;      
        if (type === 'array') {
            obj = [];
        } else if (type === 'object') {
            obj = {};
        } else {    
            // 不再具有下一層次
            return data;
        }      
        if (type === 'array') {        // eslint-disable-next-line
            for (let i = 0, len = data.length; i < len; i++) {
                obj.push(this.deepClone(data[i]));
            }
        } else if (type === 'object') {        // 對原型上的方法也拷貝了....
            // eslint-disable-next-line
            for (const key in data) {
                obj[key] = this.deepClone(data[key]);
            }
        }      
        return obj;
    }
    function  judgeType(obj) {  
        // tostring會(huì)返回對應(yīng)不同的標(biāo)簽的構(gòu)造函數(shù)
        const toString = Object.prototype.toString;      
        const map = {
            '[object Boolean]': 'boolean',
            '[object Number]': 'number',
            '[object String]': 'string',
            '[object Function]': 'function',
            '[object Array]': 'array',
            '[object Date]': 'date',
            '[object RegExp]': 'regExp',
            '[object Undefined]': 'undefined',
            '[object Null]': 'null',
            '[object Object]': 'object',
        };      
        if (obj instanceof Element) {
            return 'element';
        }      
        return map[toString.call(obj)];
    }

作者:MepJia
鏈接:https://juejin.im/post/5df0545ee51d4557ed542146
來源:掘金
著作權(quán)歸作者所有。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán),非商業(yè)轉(zhuǎn)載請注明出處。

六、第三方庫:比如 lodash 的cloneDeep,

不想自己寫就用這個(gè),100多M的庫,不用的擔(dān)心,這是開發(fā)依賴而已,只在開發(fā)的時(shí)候用到這個(gè)庫,上線時(shí)候會(huì)編譯掉的。
官網(wǎng):https://lodash.com/

  • 如果用jQuery
    jQuery 自帶的 extend 方法可以用來實(shí)現(xiàn)對象的復(fù)制。
var a = {v1:1, v2:2};
var b = {};
$.extend(b,a);
b.v1 = 3;
console.log("對象a:",a);
console.log("對象b:",b);

如果還有其他更好的深度克隆的實(shí)現(xiàn),歡迎留言交流,我也會(huì)繼續(xù)查找資料。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容