JS複習之深淺拷貝

爐火糖粥、發表於2020-12-11

一、複習導論(資料型別相關)

  想掌握JS的深淺拷貝,首先來回顧一下JS的資料型別,JS中資料型別分為基本資料型別和引用資料型別。

  基本資料型別是指存放在棧中的簡單資料段,資料大小確定,記憶體空間大小可以分配,它們是直接按值存放的,所以可以直接按值訪問。包含Number、String、Boolean、null、undefined 、Symbol、bigInt。

  引用型別是存放在堆記憶體中的物件,變數其實是儲存的在棧記憶體中的一個指標,這個指標指向堆記憶體中的引用地址。除了上面的 7 種基本資料型別外,剩下的就是引用型別了,統稱為 Object 型別。細分的話,有:Object 型別、Array 型別、Date 型別、RegExp 型別、Function 型別 等

  

 

 

   由於基本資料型別和引用資料型別儲存方式的差異,所以我們在進行復制變數時,基本資料型別複製後會產生兩個獨立不會互相影響的變數,而引用資料型別複製時,實際上是將這個引用型別在棧記憶體中的引用地址複製了一份給新的變數,其實就是一個指標。因此當操作結束後,這兩個變數實際上指向的是同一個在堆記憶體中的物件,改變其中任意一個物件,另一個物件也會跟著改變。於是在引用資料型別的複製過程中便出現了深淺拷貝的概念。

二、深淺拷貝的區別

  淺拷貝,對於目標物件第一層為基本資料型別的資料,就是直接賦值,即傳值;而對於目標物件第一層為引用資料型別的資料,就是直接賦存於棧記憶體中的堆記憶體地址,即傳地址,並沒有開闢新的棧,也就是複製的結果是兩個物件指向同一個地址,修改其中一個物件的屬性,則另一個物件的屬性也會改變。

  深拷貝,則是開闢新的棧,兩個物件對應兩個不同的地址,修改一個物件的屬性,不會改變另一個物件的屬性。

三、淺拷貝的實現方式

  1.物件的淺拷貝

    (1)Object.assign()

      ES6中新增的方法,用於物件的合併,將源物件(source)的所有可列舉屬性,複製到目標物件(target),詳細用法傳送門。程式碼示例:

let obj = {
	a:1,
	b:{
		m:'2',
		n:'3'
	},
	c:[1,2,3,4,5,6]
}

let copyObj = Object.assign({},obj)
obj.a = 5
obj.b.m = '222'

console.log(copyObj) //{ a: 1, b: { m: '222', n: '3' }, c: [ 1, 2, 3, 4, 5, 6 ] }

      上面的程式碼修改了obj內部a的值和b.m的值,但是在複製出來的物件中,a的值並未改變,m的值改變了。所以Object.assign()複製時遇到基本資料型別時直接複製值,但是遇到引用資料型別仍然複製的是地址,嚴格來講屬於淺拷貝。

   (2)迴圈遍歷

let obj = {
	a:1,
	b:{
		m:'2',
		n:'3'
	},
	c:[1,2,3,4,5,6]
}

let copyObj = {}
for(var k in obj){
	copyObj[k] = obj[k]
}

copyObj.a = 5
copyObj.b.m = '333'

console.log(obj) //{ a: 1, b: { m: '333', n: '3' }, c: [ 1, 2, 3, 4, 5, 6 ] }
console.log(copyObj) //{ a: 5, b: { m: '333', n: '3' }, c: [ 1, 2, 3, 4, 5, 6 ] }

  2.陣列的淺拷貝

   Array.concat()、Array.slice(0)、 Array.from()、擴充運算子(...)

let arr = [1,2,3,4,[5,6]]
let copyArr = arr.concat() //arr.slice(0) 、Array.from()、[...arr]
copyArr[0] = '555'
copyArr[4][1] = 7

console.log(arr) //[ 1, 2, 3, 4, [ 5, 7 ] ]
console.log(copyArr) //[ '555', 2, 3, 4, [ 5, 7 ] ]

四、深拷貝的實現方式

  1.JSON.parse()和JSON.stringify()

const obj1 = {
    x: 1, 
    y: {
        m: 1
    }
};
const obj2 = JSON.parse(JSON.stringify(obj1));
console.log(obj1) //{x: 1, y: {m: 1}}
console.log(obj2) //{x: 1, y: {m: 1}}

obj2.y.m = 2; //修改obj2.y.m
console.log(obj1) //{x: 1, y: {m: 1}} 原物件未改變
console.log(obj2) //{x: 2, y: {m: 2}}

  這種方法使用較為簡單,可以滿足基本日常的深拷貝需求,而且能夠處理JSON格式能表示的所有資料型別,但是有以下幾個缺點: 

    (1)undefined、任意的函式、正規表示式型別以及 symbol 值,在序列化過程中會被忽略(出現在非陣列物件的屬性值中時)或者被轉換成 null(出現在陣列中時);

    (2) 它會拋棄物件的constructor。也就是深拷貝之後,不管這個物件原來的建構函式是什麼,在深拷貝之後都會變成Object;

    (3) 對於正規表示式型別、函式型別等無法進行深拷貝(而且會直接丟失相應的值)

    (4) 如果物件中存在迴圈引用的情況無法正確處理。

//忽略undefined、symbol 和函式
let obj = {
    name: 'muyiy',
    a: undefined,
    b: Symbol('muyiy'),
    c: function() {}
}
console.log(obj);
// {
// 	name: "muyiy", 
// 	a: undefined, 
//  b: Symbol(muyiy), 
//  c: ƒ ()
// }

let b = JSON.parse(JSON.stringify(obj));
console.log(b);// {name: "muyiy"}

//迴圈引用情況下,會報錯
let obj = {
    a: 1,
    b: {
        c: 2,
   		d: 3
    }
}
obj.a = obj.b;
obj.b.c = obj.a;

let b = JSON.parse(JSON.stringify(obj));// Uncaught TypeError: Converting circular structure to JSON

//正則情況下
let obj = {
    name: "muyiy",
    a: /'123'/
}
console.log(obj);
// {name: "muyiy", a: /'123'/}

let b = JSON.parse(JSON.stringify(obj));
console.log(b);

  2.jQuery.extend()

    附上原始碼解析:

jQuery.extend = jQuery.fn.extend = function() { //給jQuery物件和jQuery原型物件都新增了extend擴充套件方法
  var options, name, src, copy, copyIsArray, clone, target = arguments[0] || {},
  i = 1,
  length = arguments.length,
  deep = false;
  //以上其中的變數:options是一個快取變數,用來快取arguments[i],name是用來接收將要被擴充套件物件的key,src改變之前target物件上每個key對應的value。
  //copy傳入物件上每個key對應的value,copyIsArray判定copy是否為一個陣列,clone深拷貝中用來臨時存物件或陣列的src。

  // 處理深拷貝的情況
  if (typeof target === "boolean") {
    deep = target;
    target = arguments[1] || {};
    //跳過布林值和目標 
    i++;
  }

  // 控制當target不是object或者function的情況
  if (typeof target !== "object" && !jQuery.isFunction(target)) {
    target = {};
  }

  // 當引數列表長度等於i的時候,擴充套件jQuery物件自身。
  if (length === i) {
    target = this; --i;
  }
  for (; i < length; i++) {
    if ((options = arguments[i]) != null) {
      // 擴充套件基礎物件
      for (name in options) {
        src = target[name];	
        copy = options[name];

        // 防止永無止境的迴圈,這裡舉個例子,
            // 如 var a = {name : b};
            // var b = {name : a}
            // var c = $.extend(a, b);
            // console.log(c);
            // 如果沒有這個判斷變成可以無限展開的物件
            // 加上這句判斷結果是 {name: undefined}
        if (target === copy) {
          continue;
        }
        if (deep && copy && (jQuery.isPlainObject(copy) || (copyIsArray = jQuery.isArray(copy)))) {
          if (copyIsArray) {
            copyIsArray = false;
            clone = src && jQuery.isArray(src) ? src: []; // 如果src存在且是陣列的話就讓clone副本等於src否則等於空陣列。
          } else {
            clone = src && jQuery.isPlainObject(src) ? src: {}; // 如果src存在且是物件的話就讓clone副本等於src否則等於空陣列。
          }
          // 遞迴拷貝
          target[name] = jQuery.extend(deep, clone, copy);
        } else if (copy !== undefined) {
          target[name] = copy; // 若原物件存在name屬性,則直接覆蓋掉;若不存在,則建立新的屬性。
        }
      }
    }
  }
  // 返回修改的物件
  return target;
};

    jQuery的extend方法使用基本的遞迴思路實現了淺拷貝和深拷貝,但是這個方法也無法處理源物件內部迴圈引用。

  3.lodash.cloneDeep()

    已經有大佬專門寫了lodash的原始碼解析(傳送門

  4.自己實現一個深拷貝

function deepClone(source) {
  if (!source && typeof source !== 'object') {
    throw new Error('error arguments', 'deepClone')
  }
  const targetObj = source.constructor === Array ? [] : {}
  Object.keys(source).forEach(keys => {
    if (source[keys] && typeof source[keys] === 'object') {
      targetObj[keys] = deepClone(source[keys])
    } else {
      targetObj[keys] = source[keys]
    }
  })
  return targetObj
}

  

 

參考文件:https://segmentfault.com/a/1190000015042902

     https://github.com/yygmind/blog/issues/29

      

 

相關文章