JavaScript基礎心法——深淺拷貝

axuebin發表於2017-11-01

原文地址:JavaScript基礎心法——深淺拷貝

歡迎star。

如果有錯誤的地方歡迎指正。


淺拷貝和深拷貝都是對於JS中的引用型別而言的,淺拷貝就只是複製物件的引用,如果拷貝後的物件發生變化,原物件也會發生變化。只有深拷貝才是真正地對物件的拷貝。

前言

說到深淺拷貝,必須先提到的是JavaScript的資料型別,之前的一篇文章JavaScript基礎心法——資料型別說的很清楚了,這裡就不多說了。

需要知道的就是一點:JavaScript的資料型別分為基本資料型別和引用資料型別。

對於基本資料型別的拷貝,並沒有深淺拷貝的區別,我們所說的深淺拷貝都是對於引用資料型別而言的。

淺拷貝

淺拷貝的意思就是隻複製引用,而未複製真正的值。

const originArray = [1,2,3,4,5];
const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};

const cloneArray = originArray;
const cloneObj = originObj;

console.log(cloneArray); // [1,2,3,4,5]
console.log(originObj); // {a:'a',b:'b',c:Array[3],d:{dd:'dd'}}

cloneArray.push(6);
cloneObj.a = {aa:'aa'};

console.log(cloneArray); // [1,2,3,4,5,6]
console.log(originArray); // [1,2,3,4,5,6]

console.log(cloneObj); // {a:{aa:'aa'},b:'b',c:Array[3],d:{dd:'dd'}}
console.log(originArray); // {a:{aa:'aa'},b:'b',c:Array[3],d:{dd:'dd'}}複製程式碼

上面的程式碼是最簡單的利用 = 賦值操作符實現了一個淺拷貝,可以很清楚的看到,隨著 cloneArraycloneObj 改變,originArrayoriginObj 也隨著發生了變化。

深拷貝

深拷貝就是對目標的完全拷貝,不像淺拷貝那樣只是複製了一層引用,就連值也都複製了。

只要進行了深拷貝,它們老死不相往來,誰也不會影響誰。

目前實現深拷貝的方法不多,主要是兩種:

  1. 利用 JSON 物件中的 parsestringify
  2. 利用遞迴來實現每一層都重新建立物件並賦值

JSON.stringify/parse的方法

先看看這兩個方法吧:

The JSON.stringify() method converts a JavaScript value to a JSON string.

JSON.stringify 是將一個 JavaScript 值轉成一個 JSON 字串。

The JSON.parse() method parses a JSON string, constructing the JavaScript value or object described by the string.

JSON.parse 是將一個 JSON 字串轉成一個 JavaScript 值或物件。

很好理解吧,就是 JavaScript 值和 JSON 字串的相互轉換。

它能實現深拷貝呢?我們來試試。

const originArray = [1,2,3,4,5];
const cloneArray = JSON.parse(JSON.stringify(originArray));
console.log(cloneArray === originArray); // false

const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
const cloneObj = JSON.parse(JSON.stringify(originObj));
console.log(cloneObj === originObj); // false

cloneObj.a = 'aa';
cloneObj.c = [1,1,1];
cloneObj.d.dd = 'doubled';

console.log(cloneObj); // {a:'aa',b:'b',c:[1,1,1],d:{dd:'doubled'}};
console.log(originObj); // {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};複製程式碼

確實是深拷貝,也很方便。但是,這個方法只能適用於一些簡單的情況。比如下面這樣的一個物件就不適用:

const originObj = {
  name:'axuebin',
  sayHello:function(){
    console.log('Hello World');
  }
}
console.log(originObj); // {name: "axuebin", sayHello: ƒ}
const cloneObj = JSON.parse(JSON.stringify(originObj));
console.log(cloneObj); // {name: "axuebin"}複製程式碼

發現在 cloneObj 中,有屬性丟失了。。。那是為什麼呢?

MDN 上找到了原因:

If undefined, a function, or a symbol is encountered during conversion it is either omitted (when it is found in an object) or censored to null (when it is found in an array). JSON.stringify can also just return undefined when passing in "pure" values like JSON.stringify(function(){}) or JSON.stringify(undefined).

undefinedfunctionsymbol 會在轉換過程中被忽略。。。

明白了吧,就是說如果物件中含有一個函式時(很常見),就不能用這個方法進行深拷貝。

遞迴的方法

遞迴的思想就很簡單了,就是對每一層的資料都實現一次 建立物件->物件賦值 的操作,簡單粗暴上程式碼:

function deepClone(source){
  const targetObj = source.constructor === Array ? [] : {}; // 判斷複製的目標是陣列還是物件
  for(let keys in source){ // 遍歷目標
    if(source.hasOwnProperty(keys)){
      if(source[keys] && typeof source[keys] === 'object'){ // 如果值是物件,就遞迴一下
        targetObj[keys] = source[keys].constructor === Array ? [] : {};
        targetObj[keys] = deepClone(source[keys]);
      }else{ // 如果不是,就直接賦值
        targetObj[keys] = source[keys];
      }
    } 
  }
  return targetObj;
}複製程式碼

我們來試試:

const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
const cloneObj = deepClone(originObj);
console.log(cloneObj === originObj); // false

cloneObj.a = 'aa';
cloneObj.c = [1,1,1];
cloneObj.d.dd = 'doubled';

console.log(cloneObj); // {a:'aa',b:'b',c:[1,1,1],d:{dd:'doubled'}};
console.log(originObj); // {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};複製程式碼

可以。那再試試帶有函式的:

const originObj = {
  name:'axuebin',
  sayHello:function(){
    console.log('Hello World');
  }
}
console.log(originObj); // {name: "axuebin", sayHello: ƒ}
const cloneObj = deepClone(originObj);
console.log(cloneObj); // {name: "axuebin", sayHello: ƒ}複製程式碼

也可以。搞定。

是不是以為這樣就完了?? 當然不是。

JavaScript中的拷貝方法

我們知道在 JavaScript 中,陣列有兩個方法 concatslice 是可以實現對原陣列的拷貝的,這兩個方法都不會修改原陣列,而是返回一個修改後的新陣列。

同時,ES6 中 引入了 Object.assgn 方法和 ... 展開運算子也能實現對物件的拷貝。

那它們是淺拷貝還是深拷貝呢?

concat

The concat() method is used to merge two or more arrays. This method does not change the existing arrays, but instead returns a new array.

該方法可以連線兩個或者更多的陣列,但是它不會修改已存在的陣列,而是返回一個新陣列。

看著這意思,很像是深拷貝啊,我們來試試:

const originArray = [1,2,3,4,5];
const cloneArray = originArray.concat();

console.log(cloneArray === originArray); // false
cloneArray.push(6); // [1,2,3,4,5,6]
console.log(originArray); [1,2,3,4,5];複製程式碼

看上去是深拷貝的。

我們來考慮一個問題,如果這個物件是多層的,會怎樣。

const originArray = [1,[1,2,3],{a:1}];
const cloneArray = originArray.concat();
console.log(cloneArray === originArray); // false
cloneArray[1].push(4);
cloneArray[2].a = 2; 
console.log(originArray); // [1,[1,2,3,4],{a:2}]複製程式碼

originArray 中含有陣列 [1,2,3] 和物件 {a:1},如果我們直接修改陣列和物件,不會影響 originArray,但是我們修改陣列 [1,2,3] 或物件 {a:1} 時,發現 originArray 也發生了變化。

結論:concat 只是對陣列的第一層進行深拷貝。

slice

The slice() method returns a shallow copy of a portion of an array into a new array object selected from begin to end (end not included). The original array will not be modified.

解釋中都直接寫道是 a shallow copy 了 ~

但是,並不是!

const originArray = [1,2,3,4,5];
const cloneArray = originArray.slice();

console.log(cloneArray === originArray); // false
cloneArray.push(6); // [1,2,3,4,5,6]
console.log(originArray); [1,2,3,4,5];複製程式碼

同樣地,我們試試多層的陣列。

const originArray = [1,[1,2,3],{a:1}];
const cloneArray = originArray.slice();
console.log(cloneArray === originArray); // false
cloneArray[1].push(4);
cloneArray[2].a = 2; 
console.log(originArray); // [1,[1,2,3,4],{a:2}]複製程式碼

果然,結果和 concat 是一樣的。

結論:slice 只是對陣列的第一層進行深拷貝。

Object.assign()

The Object.assign() method is used to copy the values of all enumerable own properties from one or more source objects to a target object. It will return the target object.

複製複製複製。

那到底是淺拷貝還是深拷貝呢?

自己試試吧。。

結論:Object.assign() 拷貝的是屬性值。假如源物件的屬性值是一個指向物件的引用,它也只拷貝那個引用值。

... 展開運算子

const originArray = [1,2,3,4,5,[6,7,8]];
const originObj = {a:1,b:{bb:1}};

const cloneArray = [...originArray];
cloneArray[0] = 0;
cloneArray[5].push(9);
console.log(originArray); // [1,2,3,4,5,[6,7,8,9]]

const cloneObj = {...originObj};
cloneObj.a = 2;
cloneObj.b.bb = 2;
console.log(originObj); // {a:1,b:{bb:2}}複製程式碼

結論:... 實現的是物件第一層的深拷貝。後面的只是拷貝的引用值。

首層淺拷貝

我們知道了,會有一種情況,就是對目標物件的第一層進行深拷貝,然後後面的是淺拷貝,可以稱作“首層淺拷貝”。

我們可以自己實現一個這樣的函式:

function shallowClone(source) {
  const targetObj = source.constructor === Array ? [] : {}; // 判斷複製的目標是陣列還是物件
  for (let keys in source) { // 遍歷目標
    if (source.hasOwnProperty(keys)) {
      targetObj[keys] = source[keys];
    }
  }
  return targetObj;
}複製程式碼

我們來測試一下:

const originObj = {a:'a',b:'b',c:[1,2,3],d:{dd:'dd'}};
const cloneObj = shallowClone(originObj);
console.log(cloneObj === originObj); // false
cloneObj.a='aa';
cloneObj.c=[1,1,1];
cloneObj.d.dd='surprise';複製程式碼

經過上面的修改,cloneObj 不用說,肯定是 {a:'aa',b:'b',c:[1,1,1],d:{dd:'surprise'}} 了,那 originObj 呢?剛剛我們驗證了 cloneObj === originObjfalse,說明這兩個物件引用地址不同啊,那應該就是修改了 cloneObj 並不影響 originObj

console.log(cloneObj); // {a:'aa',b:'b',c:[1,1,1],d:{dd:'surprise'}}
console.log(originObj); // {a:'a',b:'b',c:[1,2,3],d:{dd:'surprise'}}複製程式碼

What happend?

originObj 中關於 ac都沒被影響,但是 d 中的一個物件被修改了。。。說好的深拷貝呢?不是引用地址都不一樣了嗎?

原來是這樣:

  1. shallowClone 的程式碼中我們可以看出,我們只對第一層的目標進行了 深拷貝 ,而第二層開始的目標我們是直接利用 = 賦值操作符進行拷貝的。
  2. so,第二層後的目標都只是複製了一個引用,也就是淺拷貝。

總結

  1. 賦值運算子 = 實現的是淺拷貝,只拷貝物件的引用值;
  2. JavaScript 中陣列和物件自帶的拷貝方法都是“首層淺拷貝”;
  3. JSON.stringify 實現的是深拷貝,但是對目標物件有要求;
  4. 若想真正意義上的深拷貝,請遞迴。

相關文章