深入淺出深拷貝與淺拷貝

goooooooogle發表於2019-02-22

一、基本型別與引用型別

ECMAScript 中資料型別可分為:

  • 基本型別:String、Number、Boolean、Symbol、null、undefined
  • 引用型別:Object、Array、Date、RegExp、Function等

不同型別的儲存方式:

  • 基本型別:基本型別值在記憶體中佔據固定大小,儲存在棧記憶體中
  • 引用型別:引用型別的值是物件,儲存在堆記憶體中,而棧記憶體儲存的物件的變數識別符號和物件儲存在堆記憶體中的儲存地址

不同型別的複製方式:

  • 基本型別:從一個變數向另外一個新變數複製基本型別的值,會建立這個值的一個副本,並將該副本複製給新變數
let foo = 1;
let bar = foo;
console.log(foo === bar); // -> true

// 修改foo變數的值並不會影響bar變數的值
let foo = 233;
console.log(foo); // -> 233
console.log(bar); // -> 1
複製程式碼
  • 引用型別:從一個變數向另一個新變數複製引用型別的值,其實複製的是指標,最終兩個變數最終都指向同一個物件
let foo = {
  name: 'leeper',
  age: 20
}
let bar = foo;
console.log(foo === bar); // -> true

// 改變foo變數的值會影響bar變數的值
foo.age = 19;
console.log(foo); // -> {name: 'leeper', age: 19}
console.log(bar); // -> {name: 'leeper', age: 19}
複製程式碼

二、拷貝

  • 淺拷貝(一層):僅僅是複製了引用,彼此之間的操作會互相影響
  • 深拷貝(多層):在堆中重新分配記憶體,不同的地址,相同的值,互不影響

首先深複製和淺複製只針對像 Object, Array 這樣的複雜物件的。簡單來說,淺複製只複製一層物件的屬性,而深複製則遞迴複製了所有層級。

2.1 淺拷貝

2.1.1 Object.assign

// 使用Object.assign解決
// 使用Object.assign(),你就可以沒有繼承就能獲得另一個物件的所有屬性,快捷好用。 
// Object.assign 方法只複製源物件中可列舉的屬性和物件自身的屬性。
let obj = { a:1, arr:[2,3]};
let res = Object.assign({}, obj)

console.log(res.arr === obj.arr); // true,指向同一個引用
console.log(res === obj); // false
複製程式碼

2.1.2 擴充套件運算子

// 使用擴充套件運算子(…)來解決
let obj = { a:1, arr:[2,3]};
let res = {...obj};

console.log(res.arr === obj.arr); // true,指向同一個引用
console.log(res === obj); // false
複製程式碼

2.1.3 淺拷貝原生實現

const shallowCopy = (sourceObj) => {
  if (typeof sourceObj !== 'object') return;
  let newObj = sourceObj instanceof Array ? [] : {};
  
  for(let key in sourceObj){ 
    if(sourceObj.hasOwnProperty(key)) {
      //只複製元素自身的屬性,不復制原型鏈上的
      newObj[key] = sourceObj[key];
    }
  }
  return newObj;
}

let obj = { a:1, arr:[2,3]};
let res = shallowCopy(obj);
console.log(res.arr === obj.arr); // true,指向同一個引用
console.log(res.a === obj.a); // false
複製程式碼

因為淺複製只會將物件的各個屬性進行依次複製,並不會進行遞迴複製,而 JavaScript 儲存物件都是存地址的,所以淺複製會導致 obj.arr 和 shallowObj.arr 指向同一塊記憶體地址,大概的示意圖如下。

深入淺出深拷貝與淺拷貝

2.2 深拷貝

2.2.1 JSON 序列化

  • JSON.stringify():把一個 js 物件序列化為一個 JSON 字串
  • JSON.parse():把 JSON 字串反序列化為一個 js 物件
// 可以通過 JSON.parse(JSON.stringify(object)) 來解決
let a = {
    age: 1,
    jobs: {
        first: 'FE'
    }
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE
複製程式碼

但是該方法也是有侷限性的:

  1. 會忽略 undefined
  2. 不能序列化函式(會忽略函式)
  3. 不能解決迴圈引用的物件

並且該函式是內建函式中處理深拷貝效能最快的。當然如果你的資料中含有以上三種情況下,可以使用 lodash 的深拷貝函式。

2.2.2 深拷貝的原生實現

const deepCopy = (sourceObj) => {
  if(typeof sourceObj !== 'object') return;
  let newObj = sourceObj instanceof Array ? [] : {};
  
  for(let key in sourceObj){
    if(sourceObj.hasOwnProperty(key)) {
     //只複製元素自身的屬性,不復制原型鏈上的
      newObj[key] = (typeof sourceObj[key] === 'object' ? deepCopy(sourceObj[key]) : sourceObj[key]);
     }
   }
   return newObj;
}

let obj = { a:1, arr:[2,3]};
let res = deepCopy(obj);
console.log(res.arr === obj.arr); // false,指向不同的引用
console.log(res === obj); // false
複製程式碼

而深複製則不同,它不僅將原物件的各個屬性逐個複製出去,而且將原物件各個屬性所包含的物件也依次採用深複製的方法遞迴複製到新物件上。這就不會存在上面 obj 和 shallowObj 的 arr 屬性指向同一個物件的問題。

深入淺出深拷貝與淺拷貝

2.3 Array 中的拷貝

2.3.1 Array.prototype.slice()

let a = [1, 2, 3, 4];
let b = a.slice();
console.log(a === b); // -> false(當引用型別時需要滿足值相等和引用相等才為 true)

a[0] = 5;
console.log(a); // -> [5, 2, 3, 4]
console.log(b); // -> [1, 2, 3, 4]
複製程式碼

2.3.2 Array.prototype.concat()

let a = [1, 2, 3, 4];
let b = a.concat();
console.log(a === b); // -> false

a[0] = 5;
console.log(a); // -> [5, 2, 3, 4]
console.log(b); // -> [1, 2, 3, 4]
複製程式碼

2.3.3 綜上

看起來 Array 的 slice(), concat() 似乎是深拷貝,再接著看就知道它們究竟是深拷貝還是淺拷貝:

let a = [[1, 2], 3, 4];
let b = a.slice();
console.log(a === b); // -> false

a[0][0] = 0;
console.log(a); // -> [[0, 2], 3, 4]
console.log(b); // -> [[0, 2], 3, 4]
複製程式碼

同樣,對於concat()也進行驗證:

![](https://user-gold-cdn.xitu.io/2019/2/22/169156d156f7c222?w=720&h=270&f=jpeg&s=15463)
let a = [[1, 2], 3, 4];
let b = a.concat();
console.log(a === b); // -> false

a[0][0] = 0;
console.log(a); // -> [[0, 2], 3, 4]
console.log(b); // -> [[0, 2], 3, 4]
複製程式碼

綜上, Array 的 slice 和 concat 方法並不是真正的深拷貝,對於 Array 的第一層的元素是深拷貝,而 Array 的第二層 slice 和 concat 方法是複製引用。所以,Array 的 slice 和 concat 方法都是淺拷貝。

相關文章