javascript 淺拷貝VS深拷貝

Patricklea發表於2018-08-11

深拷貝VS淺拷貝

本文主要對深拷貝&淺拷貝的解釋及實現做一下簡單記錄。

之所以會有深拷貝與淺拷貝之分,是因為不同資料型別的資料在記憶體中的儲存區域不一樣。

堆和棧是計算機中劃分出來用來儲存的區域,其中堆(heap)則是動態分配的記憶體,大小不定也不會自動釋放;而棧(stack)為自動分配的記憶體空間,它由系統自動釋放。存放在棧記憶體中的簡單資料段,資料大小確定,記憶體空間大小可以分配,是直接按值存放的,所以可以直接訪問。

眾所周知,JavaScript中的資料分為(基本型別和引用型別)。五種基本型別(boolean,number,undefined,null,string,)的資料的原始值是大小確定不可變的,所以放在棧記憶體中;而引用型別(object)是放在堆記憶體中的,對應賦值的變數只是一個存放在棧記憶體中的指標,也就是一個指向引用型別存放在堆記憶體中的地址。

image-20180811123105339

引用型別比較與基本型別比較的區別

引用型別是引用的比較,例如

// a與b是兩個引用型別,但其指標指向的是兩個不同的引用
let a = [1,2,3];
let b = [1,2,3];
a===b; //false

// 引用型別的賦值操作,b的指標也是指向與a同樣的引用
let a = [1,2,3];
let b = a;
a===b; //true
複製程式碼

而基本型別間的比較是值的比較,例如

let a = 1;
let b = 1;
a===b; //true

let a = 1;
let b = a;
a===b; //true
複製程式碼

賦值操作:傳值與傳址的區別

在對基本型別進行賦值操作的時候實際是傳值,即在棧記憶體中新開一個區域給新的變數,然後再把值賦給它。所以基本型別賦值的變數是兩個互相獨立變數。

let a = 10;
let b = a;
b++;
console.log(a); //10
console.log(b); //11
複製程式碼

image-20180811140720986

而引用型別的賦值操作是傳址,只是在棧記憶體中新增了一個變數,同時賦值給這個變數的只是儲存在堆記憶體中物件的地址,也就是指標的指向。因此這兩個變數的指標指向同一個地址,相互操作也就會有影響。

let a = {};
let b = a;

a.name = 'slevin';
console.log(b.name); //'slevin'
console.log(a.name); //'slevin'
console.log(a===b); //true
複製程式碼

引用型別賦值操作

需要注意的是,引用型別的賦值並不算是淺拷貝,因為賦值操作只相當於是引用,兩個變數還是指向同一物件,任何一方改變了都會影響到另一方;但淺拷貝出來的變數與源變數已經不同,在不包含子物件的情況下(此情況即為深拷貝),一方改變不會影響到另一方。如下賦值操作:

let a = {};
let b = a;

b.name = 'slevin';
console.log(a.name); //'slevin';對b操作影響到了a
console.log(a===b); //true;因為兩者指向同一個物件
複製程式碼

淺拷貝實現方法

  1. 自定義實現方法:

    // 淺拷貝的方法
    function shallowCopy(srcObj) {
      let copy = {};
      for (let key in srcObj) {
          //只拷貝自身的屬性,__proto__上繼承來的屬性不做拷貝,也可去掉這個限制
          if (srcObj.hasOwnProperty(key)) {
              copy[key] = srcObj[key];
          }
      }
      return copy;
    }
    
    let obj = {
      name: 'slevin',
      age: 18,
      language: {
          'english': 'good',
          'mandarin': 'wonderful',
      }
    }
    
    let copy = shallowCopy(obj);
    copy.age = 28;
    
    console.log(obj.age); // 18; 並沒有改變源物件
    console.log(obj === copy); //false;兩者指向不是同一個物件
    複製程式碼
  2. 利用Object.assign(target,...sources)可將一個或多個源物件上可列舉屬性的值複製到目標物件,並返回目標物件。但注意,只能做一層屬性的淺拷貝

    let obj = {
      name: 'slevin',
      age: 18,
      language: {
          english: 'good',
          mandarin: 'wonderful',
          test: [1, 2, 3]
      },
      fn:function(){
          console.log('this:',this.name);
      }
    }
    
    let copy = Object.assign({},obj);
    //不會改變源物件
    copy.age = 22;
    //會改變源物件
    copy.language.english = 'bad';
    console.log('copy===obj:',copy===obj);//false
    console.log('copy:',copy);
    console.log('obj:',obj);
    複製程式碼
  3. 對於陣列來說,也可以使用Array.prototype.slice()方法和Array.prototype.concact()方法

    let obj = [1,2,['a','b','c']]
    let copy = obj.slice();
    // 不會改變源物件
    copy[0]=111;
    // 會改變源物件
    copy[2][0]='aaa';
    console.log('copy===obj:',copy===obj);//false
    console.log('copy:',copy);
    console.log('obj:',obj);
    複製程式碼

深拷貝及實現方法

簡單來說,深拷貝就是把物件以及物件的子物件進行拷貝。因為淺拷貝只複製了一層物件的屬性,而如果物件的數值也為引用型別,那麼拷貝過來依然是個引用地址,在拷貝物件上對子物件的值進行操作會改變原始資料中的值。

例如上面obj淺拷貝得到copy物件,如果在copy物件上改變子物件copy.language上的屬性值,會影響到源物件obj

copy.language.english = 'bad';
copy.language.mandarin = 'bad';
console.log(obj.language); // {'english': 'bad','mandarin': 'bad',}
複製程式碼

那麼如何深拷貝呢?思路就是遞迴呼叫剛剛的淺拷貝,遍歷所有值為引用型別的屬性,然後依次賦值給另外一個物件即可。

  1. 方法一,自定義實現:

    /**將源物件source深度合併到目標物件target上
    * source 源物件
    * target 目標物件,預設{}
    * deep 是否深度合併,預設true
    */
    function deepCopy (source,target={},deep=true) {
      for (let key in source){
          // 深拷貝,而且只拷貝自身屬性.(預設傳入的source為物件)
          if (deep && source.hasOwnProperty(key)){
              if (typeof(source[key])==='object'){
                  // // source[key] 是物件,而 target[key] 不是物件, 則 target[key] = {} 初始化一下,否則遞迴會出錯的
                  // if (!(target[key] instanceof Object) && (source[key] instanceof Object)){
                  // target[key] = {};
                  // }
                  // // source[key] 是陣列,而 target[key] 不是陣列,則 target[key] = [] 初始化一下,否則遞迴會出錯的
                  // if (!Array.isArray(target[key]) && Array.isArray(source[key])) {
                  // target[key] = [];
                  // }
                  // 上面的程式碼可以簡化為以下:
                  target[key] = Array.isArray(source[key]) ? [] : {};
    
                  // 遞迴執行拷貝
                  deepCopy(source[key],target[key],true);
              } else {
                  target[key] = source[key];
              }
          }
      }
      return target;
    }
    
    
    let obj = {
      name: 'slevin',
      age: 18,
      language: {
          english: 'good',
          mandarin: 'wonderful',
          test:[1,2,3]
      }
    }
    let copy = deepCopy(obj);
    copy.language.test[0] = 111;
    
    console.log('copy:',copy); //111 改變目標物件的子物件屬性值
    console.log('obj:',obj); //1 對應源物件上沒有改變
    複製程式碼
  2. 利用JSON.parse(JSON.stringify(copyObj))方法

    let obj = {
      name: 'slevin',
      age: 18,
      language: {
          english: 'good',
          mandarin: 'wonderful',
          test: [1, 2, 3]
      }
    }
    let copy = JSON.parse(JSON.stringify(obj))
    copy.language.test[0]=111;
    
    console.log('copy===obj:',copy===obj);//false
    console.log('copy.language.test[0]:',copy.language.test[0]);//111
    console.log('obj.language.test[0]:',obj.language.test[0]);//1
    複製程式碼

    但要注意,此方法有兩個缺點

    • 如果源物件屬性值有函式,無法拷貝下來
    • 無法拷貝源物件原型鏈上的屬性和方法
    let obj = {
      name: 'slevin',
      age: 18,
      language: {
          english: 'good',
          mandarin: 'wonderful',
          test: [1, 2, 3]
      },
      fn:function(){
          console.log('this:',this.name);
      }
    }
    let copy = JSON.parse(JSON.stringify(obj));
    console.log('copy===obj:',copy===obj);//false
    console.log('obj.fn:',obj.fn); //fn(){}...
    console.log('copy.fn:',copy.fn); //undefined
    複製程式碼

最後,再補一張引用型別的賦值操作、淺拷貝深拷貝對比圖,加深印象

引用型別的賦值操作、淺拷貝深拷貝對比


參考資料

相關文章