深入JavaScript基礎之深淺拷貝

楚夢浮生發表於2019-03-01

最近在學到JavaScript物件的深拷貝和淺拷貝做了一些比較,將實際開發的點和基礎點做了些小結,話不多說,開始進入主題吧。


  • 基礎認識—基本型別
  • 基礎認識—引用型別
  • 淺拷貝的實現-物件&&陣列
  • 深拷貝的實現-物件&&陣列
  • 深拷貝的實現- ES6擴充套件運算子實現物件&&陣列的深拷貝
  • 深拷貝的實現-遞迴的方法
  • 深拷貝的實現-JSON.stringify/parse的方法

基礎認識:

對於js的物件的深拷貝和淺拷貝,必須先提到的是JavaScript的資料型別,
我們先來理解一些js基本的概念 —— Javascript有五種基本資料型別(也就是簡單資料型別),它們分別是:Undefined,Null,Boolean,Number和String,並且基本型別存放在棧記憶體。還含有一種複雜的資料型別(也叫引用型別)存放在堆記憶體,就是物件(Object,Array)。
堆記憶體用於存放由new建立的物件,棧記憶體存放一些基本的型別的變數和物件的引用變數。

注意Undefined和Null的區別,Undefined型別只有一個值,就是undefined,Null型別也只有一個值,也就是null

Undefined 其實就是已宣告未賦值的變數輸出的結果

null 其實就是一個不存在的物件的結果。

JS 中的淺拷貝與深拷貝,只是針對複雜資料型別(Object,Array)的複製問題。淺拷貝與深拷貝都可以實現在已有物件上再生出一份的作用。但是物件的例項是儲存在堆記憶體中然後通過一個引用值去操作物件,由此拷貝的時候就存在兩種情況了:拷貝引用和拷貝例項,這也是淺拷貝和深拷貝的區別

複製程式碼

1. 對於基本資料型別

他們的值在記憶體中佔據著固定大小的空間,並被儲存在棧記憶體中。當一個變數向另一個變數複製基本型別的值,會建立這個值的副本,並且我們不能給基本資料型別的值新增屬性

var a = 1;
var b = a;
b.name = `hanna`;
console.log(a); //1
console.log(b.name); //undefined
複製程式碼

上面的程式碼中,a是基本資料型別(Number), b是a的一個副本,它們兩者都佔有不同位置但相等的記憶體空間,只是它們的值相等,若改變其中一方,另一方不會隨之改變。

2. 對於引用型別

複雜的資料型別即是引用型別,它的值是物件,儲存在堆記憶體中,包含引用型別值的變數實際上包含的不是物件本身,而是一個指向該物件的指標。從一個變數向另一個變數複製引用型別的值,複製的其實是指標地址而已,因此兩個變數最終都指向同一個物件

var obj = {
   name:`Hanna Ding`,
   age: 22
}
var obj2 = obj;
obj2[`c`] = 5;
console.log(obj); //Object {name: "Hanna Ding", age: 22, c: 5}
console.log(obj2); //Object {name: "Hanna Ding", age: 22, c: 5}
複製程式碼
image

注:0x123指標地址

我們可以看到obj賦值給obj2後,但我們改變其中一個物件的屬性值,兩個物件都發生了改變,根本原因就是obj和obj2兩個變數都指向同一個指標,賦值時只是複製了指標地址,它們指向同一個引用,所以當我們改變其中一個的值就會影響到另一個變數的值。

一、深拷貝和淺拷貝的區別

淺拷貝(shallow copy):只複製指向某個物件的指標,而不復制物件本身,新舊物件共享一塊記憶體;
  深拷貝(deep copy):複製並建立一個一摸一樣的物件,不共享記憶體,修改新物件,舊物件保持不變。

淺拷貝的實現

淺拷貝的意思就是隻複製引用,而未複製真正的值,有時候我們只是想備份陣列,但是隻是簡單讓它賦給一個變數,改變其中一個,另外一個就緊跟著改變,但很多時候這不是我們想要的。

(1)物件的淺拷貝
上面的程式碼就是物件的淺拷貝的例子

(2)陣列的淺拷貝

var arr = [1, 2, 3, `4`];

var arr2 = arr;
arr2[1] = "test"; 
console.log(arr); // [1, "test", 3, "4"]
console.log(arr2); // [1, "test", 3, "4"]

arr[0]="fisrt"
console.log(arr); // ["fisrt", "test", 3, "4"]
console.log(arr2); // ["fisrt", "test", 3, "4"]



複製程式碼

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

深拷貝的實現

(1)陣列的深拷貝
對於陣列我們可以使用slice() 和 concat() 方法來解決上面的問題

注意:(slice() 和 concat()對陣列的深拷貝是有侷限性的。

**slice **

var arr = [`a`, `b`, `c`];
var arrCopy = arr.slice(0);
arrCopy[0] = `test`
console.log(arr); // ["a", "b", "c"]
console.log(arrCopy); // ["test", "b", "c"]
複製程式碼

concat

var arr = [`a`, `b`, `c`];
var arrCopy = arr.concat();
arrCopy[0] = `test`
console.log(arr); // ["a", "b", "c"]
console.log(arrCopy); // ["test", "b", "c"]
複製程式碼

針對上面說的slice() 和 concat()對侷限性,我們可以繼續看下面的例子:

var arr1 = [{"name":"Roubin"},{"name":"RouSe"}];//原陣列
var arr2 = [].concat(arr1);//拷貝陣列
arr1[1].name="Tom";
console.log(arr1);//[{"name":"Roubin"},{"name":"Tom"}]
console.log(arr2);//[{"name":"Roubin"},{"name":"Tom"}]
複製程式碼

可以發現使用.concat()和淺複製的結果一樣,這是為什麼呢,那slice()會出現同樣的結果嗎?繼續看寫看例子

var arr1 = [{"name":"weifeng"},{"name":"boy"}];//原陣列
var arr2 = arr1.slice(0);//拷貝陣列
arr1[1].name="girl";
console.log(arr1);// [{"name":"weifeng"},{"name":"girl"}]
console.log(arr2);//[{"name":"weifeng"},{"name":"girl"}
複製程式碼
var a1=[["1","2","3"],"2","3"];
var a2=a1.slice(0);
a1[0][0]=0; //改變a1第一個元素中的第一個元素
console.log(a1);  //[["0","2","3"],"2","3"]
console.log(a2);   //[["0","2","3"],"2","3"]
複製程式碼

喲,也是出現同樣的結果呀,原來由於上面陣列的內部屬性值是引用物件(Object,Array),slice和concat對物件陣列的拷貝,整個拷貝還是淺拷貝,拷貝之後陣列各個值的指標還是指向相同的儲存地址.

因此,slice和concat這兩個方法,僅適用於對不包含引用物件的一維陣列的深拷貝

注(補充點):

  • arrayObj.slice(start, [end]) 該方法返回一個 Array 物件,其中包含了 arrayObj 的指定部分。不會改變原陣列
  • arrayObj.concat() 方法用於連線兩個或多個陣列。該方法不會改變現有的陣列,而僅僅會返回被連線陣列的一個副本。
    其實也就是下面實現的方式,但還是用上面的方法來實現比較簡單高效些
function deepCopy(arr1, arr2) {
   for (var i = 0; i < arr1.length; ++i) {
       arr2[i] = arr1[i];
   }
}
複製程式碼

ES6擴充套件運算子實現陣列的深拷貝

var arr = [1,2,3,4,5]
var [ ...arr2 ] = arr
arr[2] = 5
console.log(arr)  //[1,2,5,4,5]
console.log(arr2)  //[1,2,3,4,5]
複製程式碼

(2)物件的深拷貝
物件的深拷貝實現原理: 定義一個新的物件,遍歷源物件的屬性 並 賦給新物件的屬性
主要是兩種:

  • 利用遞迴來實現每一層都重新建立物件並賦值
  • 利用 JSON 物件中的 parse 和 stringify
  • ES6擴充套件運算子實現物件的深拷貝
var obj = {
  name: `FungLeo`,
  sex: `man`,
  old: `18`
}
var { ...obj2 } = obj
obj.old = `22`
console.log(obj)   ///{ name: `FungLeo`, sex: `man`, old: `22`}
console.log(obj2)  ///{ name: `FungLeo`, sex: `man`, old: `18`}
複製程式碼
複製程式碼
var obj = {
   name:`xiao ming`,
   age: 22
}

var obj2 = new Object();
obj2.name = obj.name;
obj2.age = obj.age

obj.name = `xiaoDing`;
console.log(obj); //Object {name: "xiaoDing", age: 22}
console.log(obj2); //Object {name: "xiao ming", age: 22}
複製程式碼

obj2是在堆中開闢的一個新記憶體塊,將obj1的屬性賦值給obj2時,obj2是同直接訪問對應的記憶體地址。

遞迴的方法

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

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;
}
複製程式碼

那我們來試試個例子:

var obj = {
    name: `Hanna`,
    age: 22
}
var objCopy = deepClone(obj)
obj.name = `ding`;
console.log(obj);//Object {name: "ding", age: 22}
console.log(objCopy);//Object {name: "Hanna", age: 22}

複製程式碼

物件與Json相互轉換

我們先看這兩種方法:SON.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 字串的相互轉換。

來一步步看下面的封裝層例子:

function  deepClone(origin){
    var clone={};
    try{
        clone= JSON.parse(JSON.stringify(origin));
    }
    catch(e){
        
    }
    return clone;

}
複製程式碼

未封裝和封裝的進行比較:

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 = `tt`;
 
console.log(cloneObj); 
console.log(originObj);
/****************封裝層**************/
function  deepClone(origin){
    var  clone={};
    try{
       clone= JSON.parse(JSON.stringify(origin));
    }
    catch(e){
        
    }
    return clone;

}
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 = [4,5,6];
cloneObj.d.dd = `tt`;

console.log(cloneObj); // {a:`aa`,b:`b`,c:[1,1,1],d:{dd:`tt`}};
console.log(originObj);// {a:`a`,b:`b`,c:[1,2,3],d:{dd:`dd`}};

複製程式碼

雖然上面的深拷貝很方便(請使用封裝函式進行專案開發以便於維護),但是,只適合一些簡單的情景(Number, String, Boolean, Array, Object),扁平物件,那些能夠被 json 直接表示的資料結構。function物件,RegExp物件是無法通過這種方式深拷貝。

注意

var  clone={};
    try{
       clone= JSON.parse(JSON.stringify(origin));
    }
    
    筆者在寫著段程式碼犯了一個錯誤:使用const 連續宣告瞭 導致程式碼執行出現錯誤,原因在於在同一程式碼出現連續的const宣告,則會產生暫時性死區。
    [詳細請看阮一峰的ES6](http://es6.ruanyifeng.com/#docs/let#const-命令)
    const clone={};
    try{
      const clone= JSON.parse(JSON.stringify(origin));
    }
    
複製程式碼

舉例:

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

發現在 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).

undefined、function、symbol 會在轉換過程中被忽略。。。
明白了吧,就是說如果物件中含有一個函式時(很常見),就不能用這個方法進行深拷貝。

相關文章