天天看點

關于深拷貝與淺拷貝問題的個人了解

以下内容僅為個人的了解,不一定完全正确喔 !
let foo = 'Hello';

let bar = foo;

bar = 'Hello World!';

console.log(foo); // 'Hello'

console.log(bar); // 'Hello World!'

let obj1 = {
  a: 1,
  b: {
    c: 2
  }
};

let obj2 = obj1;

obj2.a = 2;
console.log(obj1.a); // 2           

複制

這是我大一上學期遇到的一個“怪現象”,之是以說是“怪現象”,是因為當時不懂這裡面的原因,隻因為它沒有按照我的預想結果那樣。

這個結果不像上面的字元串 String 那樣,是以當時的我感到很困惑。

終于,立志不再隻當個 API Caller,開始去了解底層原理的我,終于在今天了解到産生這個的問題的原因。

是以,還是很有必要記錄一下我的對這個問題的了解。

根本原因

這個問題的根本原因就是深拷貝和淺拷貝其在記憶體中的儲存類型不同。

棧與堆

首先要了解一個概念:棧與堆。

關于這個問題,有篇知乎可以讓我們很好的了解這兩者的關系以及差別:

什麼是堆?什麼是棧?他們之間有什麼差別和聯系?

棧區(stack):系統自動配置設定的記憶體空間,有系統自動釋放。

堆區(heap):動态配置設定的記憶體空間,大小不确定,也不會自動釋放。

JavaScript 資料類型

當我們了解這堆和棧概念後,再複習一下一個很簡單的入門概念:JavaScript 資料類型

我們都知道,ECMAScript 标準定義了 7 種資料類型,其中 6 種為基本類型(原始類型),1 種為引用類型:

6 種為基本類型:

  • Boolean

  • Null

  • Undefined

  • Number

  • String

  • Symbol

    (ES6 新加入,表示獨一無二的值)

以及 1 種引用類型:

  • Object

基本類型與引用類型的差別

厘清楚兩者的關系後,我們來看看它們之間的差別。

基本類型

基本類型的變量是存放在棧記憶體(Stack)裡

具體表現如下:

  • 基本資料類型的值是按值通路的。
let x, y;

  x = 'foo';

  y = x;

  console.log(x); // "foo"

  console.log(y); // "foo"

  y = 'bar'; // 這裡給 y 重新指派。

  console.log(x); // "foo"

  console.log(y); // "bar"           

複制

  • 基本類型的值是不可變的。
let example = 'Hello World!';

  example.toUpperCase(); // HELLO WORLD!

  console.log(example); // Hello World!           

複制

  • 基本類型的比較是它們的值的比較。
let foo = 1;

  let bar = true;

  console.log(foo == bar); // true 運算符`==`表示隻進行值的比較,所有這裡在比較前進行了 隐式轉換。

  console.log(foo === bar); // false 運算符`===`則表示不僅進行值的比較,還會進行類型的比較,(Numebr 與 Boolean)。           

複制

引用類型

引用類型的值則是存放在堆記憶體(Heap)裡

具體表現如下:

  • 引用資料類型的值是按指針通路的。

變量實際上是一個存放在棧記憶體的指針,這個指針指向堆記憶體中的位址。

  • 引用類型的值是可變的。
let example = 1, 2, 3;

  example0 = 6;

  console.log(example0); // 6   Array example 的值可以被修改。           

複制

  • 引用類型的比較是引用的比較。
let x = 1, 2, 3;

  let y = 1, 2, 3;

  console.log(x === y); // false  因為兩者在記憶體中的位置不同。           

複制

傳值與傳址

在了解完上面的概念後,我們大緻明白了基本類型與引用類型的差別。

是以也不難了解這兩個概念:

傳值

傳址

顯然,根據以上的例子,我們可以得出結論:

  • 基本資料類型在指派操作時是 傳值

在指派操作時,基本資料類型的指派是在記憶體裡開辟了一段新的棧記憶體,然後再把值指派到新開辟的棧記憶體中。

  • 引用資料類型在指派操作時是 傳址

在指派操作時,引用類型的指派隻是改變了指針的指向,在給變量指派時,是把對象儲存在棧記憶體中的位址指派給變量,結果是兩個變量指向同個棧記憶體位址。

可能這麼看起來有點繞,Show the Code:

// 基本資料類型的指派操作:傳值。

let x = 6;

let y = x; // 新開辟棧記憶體,再指派到該棧記憶體中。

x += 2;

console.log(a); // 8

console.log(b); // 6           

複制

// 引用資料類型的指派操作:傳址。

let x = {}; // x 儲存了一個空對象的執行個體。

let y = x; // x 和 y 都指向了這個空對象 (改變了 y 指針指向,指向了與 x 同個位址)。

x.name = 'jovi';

console.log(x.name); // 'jovi'

console.log(x.name); // 'jovi'

b.age = 21;

console.log(b.age); // 21

console.log(a.age); // 21

console.log(a == b); // true 因為兩者在記憶體中的位置相同。           

複制

淺拷貝

上面鋪墊了這麼長,為的就是讓大家更好的了解其中的原理。

那麼為了實作文章一開頭我們想要的效果,因為上面的

傳址

操作無法滿足我們的需求,是以我們可以通過

淺拷貝

去解決。

通過擴充運算符

...

實作淺拷貝

let obj1 = {
  a: 1,
  b: {
    c: 2
  }
};

let { ...obj2 } = obj1;

obj2.a = 6;

console.log(obj1.a); // 1

console.log(obj2.a); // 6           

複制

需要注意的是:在解構指派拷貝中,一個鍵的值是複合類型的值(數組、對象、函數)、那麼解構指派拷貝的是這個值的引用,而不是這個值的副本,如下所示:

// 通過解構指派進行淺拷貝:因為鍵b(?)的值是對象,是以解構指派拷貝的是這個值的引用。
let obj1 = {
  a: 1,
  b: {
    c: 2
  }
};

let { ...obj2 } = obj1;

obj2.b.c = 6;

console.log(obj1.b.c); // 6

console.log(obj2.b.c); // 6           

複制

通過

Object.assign()

實作淺拷貝

// 淺拷貝
let obj1 = {
  a: 1,
  b: {
    c: 2
  }
};

let obj2 = obj1;

let obj3 = Object.assign({}, obj1);

obj3.a = 2;

obj2.a = 3;

console.log(obj1.a); // 3

console.log(obj2.a); // 3

console.log(obj3.a); // 2           

複制

我們通過

Object.assign()

即可實作對象的淺拷貝,如上所示。

Object.assign()

方法用于将所有可枚舉屬性的值從一個或多個源對象複制到目标對象。它将傳回目标對象。

Object.assign(target, ...sources)

接受的第一個參數

target

為目标對象,後面的參數都是源對象。

需要注意的是:拷貝的屬性是有限制的,隻拷貝源對象的自身屬性(不拷貝繼承屬性),也不拷貝不可枚舉的屬性(enumerable: false)。

同樣還有一點就是:如果源對象某個屬性的值是對象,那麼目标對象拷貝得到的是這個對象的引用,如下所示:

// 淺拷貝,且源對象obj1.b為對象,則obj3拷貝得到的是這個對象的引用。
let obj1 = {
  a: 1,
  b: {
    c: 2
  }
};

let obj3 = Object.assign({}, obj1);

obj3.b.c = 6;

console.log(obj1.b.c); // 6

console.log(obj3.b.c); // 6           

複制

這就意味着,我們還需要進行“更進一步”的拷貝。

深拷貝

通過

Lodash

cloneDeep

API 實作深拷貝(推薦)

Lodash

是一個一緻性、子產品化、高性能的 JavaScript 實用工具庫。
let _ = require('lodash');

let obj1 = {
  a: 1,
  b: {
    c: 2
  }
};

let obj2 = _.cloneDeep(obj1);

obj2.b.c = 6;

console.log(obj1.b.c); // 2

console.log(obj2.b.c); // 6           

複制

通過

JSON.parse(JSON.stringify())

進行深拷貝

// 通過JSON.parse(JSON.stringify())進行深拷貝
let obj1 = {
  a: 1,
  b: {
    c: 2
  }
};

let obj2 = JSON.parse(JSON.stringify(obj1));

obj2.b.c = 6;

console.log(obj1.b.c); // 2

console.log(obj2.b.c); // 6           

複制

需要注意的是,這個方法具有局限性:

  • 拷貝時會忽略

    undefined

  • 拷貝時會忽略

    symbol

  • 不能序列化函數
  • 不能解決循環引用的對象

最後

終于寫完了,也終于解決了很久之前遇到的問題!

希望這篇文章能夠讓你更好的了解

深拷貝

淺拷貝