詳解js深淺複製

前言

在以前寫繼承的過程談到了深淺複製的問題,由於有讀者反映到須要解析,趁今天週末寫一篇解析,今天的主體相對以前來講理解難度低一些,篇幅可能也比較短,諸君按需閱讀便可。數組

從兩種數據類型提及

在js中,變量的類型能夠大體分紅兩種:基本數據類型和引用數據類型,其中基本數據類型指的是簡單的數據段,包括:函數

  • Undefined
  • Null
  • Boolean
  • Number
  • String(字符串在一些其餘語言中是被當作對象使用的,屬於引用類型,但在js裏是基本類型)

而引用類型的值指的是可能包含多個值的對象。可能上面這種描述你們都看過很多,可是有沒有思考過爲何要把數據類型這樣分呢?本質上,是由於基本數據類型保存在棧內存,而引用類型保存在堆內存中。那再進一步問:爲何要分兩種保存方式呢? 根本緣由在於保存在棧內存的必須是大小固定的數據,引用類型的大小不固定,只能保存在堆內存中,可是咱們能夠把它的地址寫在佔內存中以供咱們訪問。舉個例子:spa

var a = 1;//定義了一個number類型
var obj1 = {//定義了一個objr類型
    name:'obj'
};

在執行這段代碼後,內存空間裏是這樣的:
棧內存和堆內存
由於這種保存方式的存在,因此咱們在操做變量的時候,若是是基本數據類型,則按值訪問,操做的就是變量保存的值;若是是引用類型的值,咱們只是經過保存在變量中的引用類型的地址類操做實際對象。從而也引出了所謂的深淺複製問題。prototype

深複製和淺複製

不一樣的複製方式

緊接着上文的內容,假設有如下代碼:code

//例子1
var a = 1;
var b = a;//複製
console.log(b)//1
a = 2;//改變a的值
console.log(b)//1

能夠看到,咱們複製完b之後,即便改變a的值,b也不會改變,由於a和b是相互獨立的,按照上面的圖,也就是在棧內存中建立了一個變量b 保存的值也是2;對象

//例子2
var color1 = ['red','green'];
var color2 = color1;//複製
console.log(color2)//['red','green'];
color1.push('black') ;//改變color1的值
console.log(color2)//['red','green','black']

在例子2中,咱們按照徹底相同的步驟,操做了一個數組,可是返回的結果卻徹底不同,由於此時的複製,其實是這樣:
數組的複製
咱們只是複製了一次引用類型的地址而已,因此,無論接下來咱們是操做color1仍是color2,本質上都是操做同一個數組對象blog

深複製和淺複製

剛剛說到,簡單的賦值沒有辦法複製引用類型,那若是咱們就是想複製上面的color1數組怎麼辦呢?能夠這樣:繼承

var color1 = ['red','green']; 
var color2 = [];
//複製
for(var i  = 0;i < color1.length;i++){
    color2[i] = color1[i]; 
}
console.log(color2)//['red','green'];
color1.push('black') ;//改變color1的值
console.log(color2)//['red','green']

這一次咱們先建立了一個空數組color2,而後讓color2的每一個值都和color1對應相等,最後的color1color2是相互獨立的了,知足了咱們的須要。固然對於對象類型也是同樣的,使用for-in遍歷取代這裏的for循環便可。遞歸

問題真的就這樣解決了嗎?固然沒有,不過以上這種只複製了第一層屬性的方式就叫作淺複製,淺複製有什麼缺陷呢?咱們能夠先思考一下,從直接使用=符號賦值進行復制到淺複製,可以複製成功(成功是指複製的結果與複製源徹底獨立),是由於咱們複製的對象都是基本類型,怎麼解釋呢?內存

  • 在複製基本數據類型時,咱們直接使用=完成複製
  • 在引用類型的時候,咱們循環遍歷對象,對每一個屬性或值使用=完成複製

有沒有注意到上文的color1例子使用淺複製之因此可以複製成功,是由於數組中的每一項都是基本數據類型(string),因此猜出了淺複製的侷限了嗎?假如數組中某一項保存的是一個對象,或者是一個數組,又或者對象的某個屬性仍是一個對象呢?(換句話說就是引用類型的某個屬性仍是引用類型),如:

var person = {
    name:'lin',
    score:{
        physics:85,
        math:99
    }
}

這個對象的分數score屬性就仍是一個對象,那咱們使用前面提到for-in遍歷複製的時候,對score的複製,不就又變成了咱們前面提到的只複製了地址的狀況嗎?再想一想淺複製實現的原理,相信你們猜到了深複製實現的方式:對屬性中全部引用類型的值,遍歷到是基本類型的值爲止,從這種方式上,咱們很容易就能夠想到利用遞歸來實現深複製。

function deepCopy (obj) {
    var result;

    //引用類型分數組和對象分別遞歸
    if (Object.prototype.toString.call(obj) == '[object Array]') {
      result = []
      for (i = 0; i < obj.length; i++) {
        result[i] = deepCopy(obj[i])
      }
    } else if (Object.prototype.toString.call(obj) == '[object Object]') {
      result = {}
      for (var attr in obj) {
        result[attr] = deepCopy(obj[attr])
      }
    }
    //值類型直接返回
    else {
      return obj
    }
    return result
}

上面的函數很簡單:對於傳入的參數,首先判斷是否爲引用類型,若是不是,直接返回便可;若是是,循環遍歷該對象的屬性,若是某個屬性仍是引用類型,則針對該屬性再次調用deepCopy函數,從而完成深複製。

附註

對於淺複製,其實還有其餘的實現方式,好比數組中concatslice方法,對於這些仍是但願你們本身瞭解,本本主要針對深淺複製的實現原理進行解析。

小結

對於深淺複製的區別,其實核心的關鍵點就是是隻複製了第一屬性仍是徹底複製了全部的屬性,可能有些地方寫的稍顯囉嗦或者描述不當,歡迎提出意見和建議(然而我並不必定會聽,哈哈)。若是對讀者有幫助,仍是但願能點個推薦。以上內容屬於我的看法,若是有不一樣意見,歡迎指出和探討。請尊重做者的版權,轉載請註明出處,如做商用,請與做者聯繫,感謝!

相關文章
相關標籤/搜索