對深拷貝挺用心的一次總結

leywis發表於2018-11-17

理解淺拷貝和深拷貝的

所謂拷貝就是複製,JS中資料型別分為基本資料型別(按值傳遞)和引用資料型別(按引用傳遞),所以在複製過程會有所不同。 為了方便理解,我們可以把複製資料類比為效仿別人開賓館

基本資料型別複製圖解(桌椅板凳可以照搬)

我們現在想開一家和賓館A一樣的賓館B(複製資料),我們首先需要把賓館A基本設施桌椅板凳(基本資料型別)拿過來。

基本資料型別圖解

引用資料型別(房間照搬不了,只能拿個房卡)

好了,複製工程的大頭來了,房間照搬不可能,為了省勁兒,直接把賓館A的房卡(引用資料型別在棧中的引用)照搬了一份。

勉強看來,開賓館B的任務算是完成了。開業把!但是這時候客人來了拿到的房卡,但是這房卡是賓館A的,開門進去萬一看到一些不可描述的事情(資料的改動)...豈不是尷尬!

引用資料型別圖解

我們發現照搬房卡(淺拷貝)行不通,於是只能自己建房間,從賓館A的房間一個個挨著進去,一個個的桌椅板凳的搬(複製),這樣才能建立自己賓館B的房間(深拷貝)。

深拷貝的定義 - 背下來應付面試官

深拷貝的過程就是指建立一個和原資料內容和結構一模一樣的新物件,新物件內的所有值的指標地址都是新的地址

程式碼層面看淺、深拷貝

檢測深淺拷貝:改變新資料, 都不改變資料

/**
* 基本資料型別拷貝
**/ 
var num1 = 5;           // 資料a
var num2 = num1;        // 把a的值複製給b
console.log(num2)       // 5

// 當我們改變num2,看是否會改變原資料a
num2 = 2;
console.log(num1)  // 5

複製程式碼

結論:基本資料型別都是深拷貝(直接複製即可)


/**
* 引用資料型別拷貝
**/ 
var obj = {               // 原始資料obj
    name: 'nike',
    age: 18,
    sex: '男'
}
// ** 淺拷貝 **
var shallowCopyObj = obj;         // 把原始物件直接複製給新物件shallowCopyObj,此處完成引用複製,也就是淺拷貝
console.log(shallowCopyObj.name);  // nike
shallowCopyObj.name = 'jack';     // 改變新物件的name屬性
console.log(obj.name);            // 'jack'  (原資料的name屬性也被改變)

// ** 深拷貝 **
var deepCopyObj = {};              // 深拷貝的思路:拆分原資料,把原資料的每個屬性單獨複製
for(let item in obj){
    deepCopyObj[item] = obj[item];
}
console.log(deepCopyobj.name);     // nike
deepCopyobj.name = 'mary';         // 複製完成後,改變deepCopyObj的name屬性
console.log(obj.name);            // 'nike'  (原資料的name屬性沒有被改變)

複製程式碼

結論:引用資料型別不能直接複製,需要遍歷屬性,分別複製。而對於屬性值有引用資料型別的多層巢狀物件,需要進行屬性值資料型別檢測,如果是引用資料型別,再繼續遍歷該屬性值,進行復制。

JS實現深拷貝

為了深拷貝多層巢狀物件,使用遞迴複製原資料

/**
* @param {Object} initData 準備要複製的物件
* @param {Object} copyData 複製的載體物件,可以為空,為空的話表示要複製到一個空物件或者空陣列上
***/
function deepClone(initData,copyData){
    var copyData = copyData || {};          // copyData初始化

    for(let item in initData){ // 遍歷
        if(typeof initData[item] === 'object'){
            copyData[item] = (initData[item].constructor == Object) ? {} : [];
            arguments.callee(initData[item],copyData[item]);
        }else{
            copyData[item] = initData[item];
        }
    }
    
    return copyData
}
複製程式碼

以上方法就實現了對多層巢狀物件的深拷貝,為了增加方法的健壯性,我們可以增加對引數型別的檢測,以及對複製ES6中map,set物件的支援

/**
* @param {Object} initData 準備要複製的物件
* @param {Object} copyData 複製的載體物件,可以為空,為空的話表示要複製到一個空物件或者空陣列上
***/
function deepClone(initData, copyData) {
    if (typeof initData !== 'object') { return initData }; // 如果要複製的資料是基本資料型別,則直接返回

    var copyData = copyData || {};          // copyData初始化
    
    switch (initData.constructor) {
        case Array:
            copyData = [];
        case Object:
            for (var property in initData) {
                copyData[property] = typeof initData[property] === 'object' ? arguments.callee(initData[property]) : initData[property];
            }
            break;
        case Map:
            copyData = new Map();
            initData.forEach((value, key) => {
                copyData.set(key, typeof value === 'object' ? arguments.callee(value) : value);
            });
            break;
        case Set:
            copyData = new Set();
            initData.forEach(value => {
                copyData.add(typeof value === 'object' ? arguments.callee(value) : value);
            });
            break;
    }
    
    return copyData
}
複製程式碼

實現深拷貝的奇技淫巧

以下記錄幾種可以實現深拷貝但是使用場景有所限制的方法,在某些場景還是可以簡化我們的工作

1.陣列只進行一層深拷貝(子元素都是基本資料型別)

  • slice() :擷取陣列返回一個新的陣列,包含從 start 到 end (不包括該元素)的元素。思路就是擷取整個陣列。
var arr = [1,2,3,4];
var newArr = arr.slice();   // 當slice()不帶任何引數的時候,預設返回一個長度和原陣列相同的新陣列
newArr[0] = 9;
console.log(arr);           // [1,2,3,4]
複製程式碼
  • concat(arr):合併arr返回一個新的陣列。該陣列是通過把所有 arr 引數新增到 原資料 中生成的。所以我們只需要合併和空陣列,就能實現對原陣列的拷貝。
var arr = [1,2,3,4];
var newArr = arr.concat();   // concat()不帶任何引數的時候,相當於合併和空陣列
newArr[0] = 9;
console.log(arr);           // [1,2,3,4]
複製程式碼

2.物件只進行一層拷貝 (子元素都是基本資料型別) 以下兩個方法都是es6,慎用

  • Object.assign(target, ...sources):該方法用於將所有可列舉屬性的值從一個或多個源物件複製到目標物件target。它將返回目標物件。所以我們可以把目標物件target設定成空物件,完成對物件的拷貝
var person = {
    name:'nike',
    age:18
}

var newPerson = Object.assign({},person);
newPerson.age = 20;
console.log(person.age);    // 18
複製程式碼
  • 擴充運算子(...):作用就是把陣列或類陣列物件展開成一系列用逗號隔開的值
var person = {
    name:'nike',
    age:18
}

var newPerson = {...person};
newPerson.age = 20;
console.log(person.age);    // 18
複製程式碼

3.用JSON.stringify把物件轉成字串,再用JSON.parse把字串轉成新的物件。

用JSON.stringify把物件轉成字串,再用JSON.parse把字串轉成新的物件。可以脫離原物件的引用,也可以實現對多層巢狀物件的深拷貝。

缺點:這種方法能正確處理的物件只有 Number, String, Boolean, Array等能夠被 json 直接表示的資料結構。而如function、正則物件等都無法正確完成轉換。

var person = {
    name:'nike',
    age:18
}

var newPerson = JSON.parse(JSON.stringify(person));
newPerson.age = 20;
console.log(person.age);    // 18

// 當屬性中存在function等
var people = {
    name:'mary',
    age:18,
    study:function(){
        console.log(1)
    }
}

var newPeople = JSON.parse(JSON.stringify(people));
console.log(newPeople.study);    // undefined
複製程式碼

結語

加入掘金也有兩年多了,但是也只是看別人的文章,自己也沒動手總結過!在這次找工作面試的過程中,感觸也挺多。

  • 有些你認為很簡單東西,別人問你的時候你還真不一定說得清

  • 有些工作中碰到的問題,不總結記錄下,著實容易忘記,面試的時候被問住了,反倒顯得自己是編的

  • 基礎真的很重要,即使在你工作絕大部分的時間你用不到,但是它決定你成長的高度

所以我也想著藉著掘金這個優秀的平臺,一方面記錄下自己學習和工作經驗,另一方面也為以後的職業發展打點基礎。

相關文章