javascript中的深淺拷貝

理解深淺拷貝,首先要明白兩種賦值方式: 傳值賦值和引用賦值,而傳值賦值和引用賦值的區別就是「值」的類型,傳值賦值中的值是基本類型,包括六種:string, number, bool, null, undefined, symbol;引用賦值中的值是對象,好比function, object, array等等,而要理解傳遞賦值和引用賦值的區別,就不得不說下javascript中的堆(heap)和棧(stack)javascript

什麼是堆棧

js中內存主要分爲棧內存和堆內存,一般棧內存存放的是對象的地址,而堆內存存放的是對象的具體內容。對於基本類型來講,其地址和內容都存在棧內存中,而引用類型其地址存在棧內存中,其內容則存在堆內存中。java

棧內存和堆內存區別。棧內存運行效率比較高,空間相對比較小,堆內存則相反。因此將構造簡單的基本類型放在棧內存中,而將複雜的引用類型放在堆內存中。數組

盜圖一張

變量名和內存地址

咱們知道了不一樣的數據類型存儲在不一樣的內存區域,那麼變量名是如何和該內存地址作一一關聯的呢?任何高級語言都有編譯的過程,而這個編譯的過程就會創建變量名和內存地址的一一對應關係,變量名只是方便咱們使用而已,在編譯後,計算機實際使用的是內存地址在操做。(大概原理,編譯原理也都忘了)函數

傳值賦值和引用賦值

結合上面的堆棧舉個栗子code

基本類型對象

var a = "a"; //基本類型,爲a分配棧內存地址和存儲內容
var b = a; //基本類型,爲b分配新的棧內存地址和內容
a = "a1"; //a對應內存地址內容修改成'a1',b對應的內存地址內容不變
console.log(a, b); //a1, a

引用類型遞歸

var a = {name: 'a'}; //引用類型,爲a分配棧內存地址,爲對象{name: 'a'}分配堆內存空間,並將內存地址做爲a地址的內容
var b = a; //引用類型,爲b分配棧內存空間,將a中對象引用的地址存到b地址中
a.name = 'a1'; //根據a用存放的地址引用,修改堆內存中對象的屬性
console.log(a.name, b.name, a===b); //此時a和b仍舊同時引用同一個對象, ===直接對比的內存地址

深拷貝和淺拷貝

深拷貝和淺拷貝不一樣的地方,就是對對象屬性操做的不一樣。簡單來講,淺拷貝,新生成對象對象屬性是源對象的引用,共同指向同一塊堆內存;深拷貝,新生成的對象屬性是一個新對象,裏面屬性和源對象中的一致。圖片

在操做層面,淺拷貝,只須要拷貝源對象的第一層屬性;而深拷貝則不斷遞歸遍歷源對象中的對象屬性,遇到對象則先新建一個對象,並將源對象中基本類型中賦值給新建對象。ip

舉個栗子內存

淺拷貝

var a = {name: 'a', toys: ['dog', 'cat']};
function simpleClone(obj) {
    var _obj = {};
    for(var p in obj) {
        _obj[p] = obj[p]; //只拷貝第一層
    }
    return _obj;
}
var b = simpleClone(a);
a.toys.push('bird');
console.log(a.toys, b.toys, a.toys===b.toys); //["dog", "cat", "bird"] ["dog", "cat", "bird"] true

從上面能夠看出修改了a以後,b也隨之改動,說明在淺拷貝中,對象屬性是在目標和源之間是同一個引用。

深拷貝

var a = {name: 'a', toys: ['dog', 'cat']};
function simpleDeepClone(obj) {
    var _obj = {};
    for (var p in obj) {
        //對象類型,遞歸處理,其餘對象類型暫不處理
        if(obj[p].constructor === object) {
            _obj[p] = simpleDeepClone(obj[p])
        } else {
            //基本類型
            _obj[p] = ojb[p]
        }
    }
    return _obj    
}

從上面能夠看出深拷貝中對於對象屬性總會有一個new的過程,因此新生成的屬性和源屬性再也不是同一個引用。

lodash中的深淺拷貝

淺拷貝

function baseClone(value) {
    if (!isObject(value)) { //非object和function直接返回,基本數據類型直接返回
      return value;
    }

      if (isArr) {//數組對象
        result = initCloneArray(value); //調用該數組對象的構造函數,構造出等長的新空數組
        if (!isDeep) {
          return copyArray(value, result);//遍歷源數組值,依次賦值給新的數組
        }
      }

    if (tag == objectTag || tag == argsTag || (isFunc && !object)) {
          result = initCloneObject(isFunc ? {} : value); //函數對象都返回空對象
          if (!isDeep) {
            /**baseAssign就是遍歷源對象屬性依次賦值給上面新建立的空對象,因爲symbol類型做爲屬性的特殊性,它不能被for...in,for...of,Object.keys(),Object.getOwnPropertyNames()返回,只能經過Object.getOwnPropertySymbols這個特殊獲取,因此須要再次單獨處理一下**/
            return copySymbols(value, baseAssign(result, value)); 
          }
        }
    }
  }

因此lodash淺拷貝的整個流程 1)若是源爲基本類型直接賦值(包括symbol),數組對象建立一個內容同樣的新數組 2)若是源爲對象且原型爲object或function,則依次變量其屬性,將其賦值給新建立的空對象(這個過程當中基本類型屬於直接賦值,對象屬性則屬於引用賦值)

深拷貝

深拷貝就是遍歷執行淺拷貝的流程(淺拷貝遇到對象會生成一個新對象,深拷貝的屬性若是是對象,就將該對象做爲淺拷貝的源,依次遍歷。)

javascript中容易忽略的深淺拷貝

  1. 函數參數都是傳值賦值,不管對象與否

  2. 不改變原數組方法

  • slice,map, filter等方法都是返回這個新數組,可是新數組是源數組的淺拷貝(修改對象元素,源會跟着改變)
var a = [{name: 'a'}, 1,2,3]
var b = a.slice(0, 2);
console.log(a,b) //[ { name: 'a' }, 1, 2, 3 ] [ { name: 'a' }, 1 ]
b[0].name = 'b';
console.log(a,b)//[ { name: 'b' }, 1, 2, 3 ] [ { name: 'b' }, 1 ]

var c = a.map(function(key) {
    if(typeof key == 'object') {
        return key.name = 'c'
    }else {
        return key
    }
})
console.log(c, a)//[ { name: 'c' }, 1, 2, 3 ] [ { name: 'c' }, 1, 2, 3 ]
相關文章
相關標籤/搜索