首頁 > 軟體

JavaScript 賦值,淺複製和深複製的區別

2022-05-25 22:01:21

前言:

淺複製和深複製可以說是面試中很常見的一道題了,本文就來聊一聊JavaScript中的淺複製和深複製。

一、變數賦值

不知道會不會有人會和我一樣,會覺得淺複製就是通過=操作符將一個變數賦值給另外一個變數。但實際上,淺複製和變數賦值之間是存在區別的。所以,我們先來了解一下變數賦值。

1.1 原始值和參照值

原始值(primitive value):最簡單的資料,即:Undefined、Null、Boolean、Number、String、BigInt、Symbol這其中型別的值。儲存原始值的變數是按值存取的,我們操作的就是儲存在變數中的實際值。

參照值(reference value):有多個值構成的物件,即 Object 型別。參照值是儲存在記憶體中的物件,但是 JavaScript 不允許直接存取記憶體位置,所以不能直接操作物件所在的記憶體空間。在操作物件時,實際操作的是該物件的參照(reference) 而非實際的物件本身。儲存參照值得變數實際上儲存得是物件得參照,是按參照存取得。

1.2 賦值

首先說明這裡說的賦值,不是直接把參照值(例如:{})或者原始值(例如:false、1、"str"等)直接賦值給一個變數。而是通過變數把一個值賦值給另一個變數。

原始值賦值:儲存原始值的變數是按值存取的,所以通過變數把一個原始值賦值給另一個變數時,原始值會被複制到新變數的位置。

let num1 = 5;
let num2 = num1;
console.log(num1, num2);  // 5 5
num2 = 4;
console.log(num1, num2);  // 5 4

可以看出 num2 通過 num1 被賦值為5,儲存的是同一個原始值。而且兩個變數相互獨立,互不干擾。

具體賦值過程如下:

參照值賦值:儲存參照值的變數是按參照存取的,通過變數把一個參照賦值給另一個變數時,儲存在變數中的值也會被複制到新變數的位置。但是,這裡複製的實際上是一個指向儲存在堆記憶體中物件的指標。賦值後,兩個變數實際上指向同一個物件。所以兩個變數通過參照對物件的操作會互相影響。

let obj1 = {};
let obj2 = obj1;
console.log(obj1);  // {}
console.log(obj2);  // {}
obj1.name = 'haha';
console.log(obj1);  // { name: 'haha' }
console.log(obj2);  // { name: 'haha' }
obj1.age = 24;
console.log(obj1);  // { name: 'haha', age: 24 }
console.log(obj2);  // { name: 'haha', age: 24 }

如上程式碼,通過 obj1 將指向物件的參照賦值給 obj2後, obj1 和 obj2 儲存了指向同一物件的參照,所以操作的是同一物件。

具體可見下圖:

注:如上兩圖來自《JavaScript 高階程式設計(第四版)》

接下來要說的淺複製和深複製就是針對參照值而言的。

二、淺複製(Shallow Copy)

我們先來看一篇部落格中對於淺複製的定義:

An object is said to be shallow copied when the source top-level properties are copied without any reference and there exist a source property whose value is an object and is copied as a reference. If the source value is a reference to an object, it only copies that reference value to the target object.

對此,個人的理解淺複製就是複製該物件的的每個屬性,如果該屬性值是原始值,則複製該原始值,如果屬性值是一個物件,那麼就複製該物件的參照。

即:淺複製將複製頂層屬性,但巢狀物件在原始(源)和拷貝(目標)之間共用

2.1 原生 JavaScript 中的淺複製

Object.assign()

Object.assign()方法將所有可列舉(Object.propertyIsEnumerable()返回 true)和自有(Object.hasOwnProperty()返回 true)屬性從一個或多個源物件複製(淺複製) 到目標物件,返回修改後的物件。

如下程式碼可以看出,淺複製和變數賦值不同,修改物件的屬性值互不影響。

const source = { a: 1, b: 2 };
const objCopied = Object.assign({}, source);

console.log(objCopied); // { a: 1, b: 2 }

source.a = 3;
console.log(source);  // { a: 3, b: 2 }
console.log(objCopied);  // { a: 1, b: 2 }

objCopied.a = 4;
console.log(source);  // { a: 3, b: 2 }
console.log(objCopied);  // { a: 4, b: 2 }

物件內的巢狀物件在源物件和拷貝物件之間還是共用的,如上程式碼,修改物件內物件的屬性時會相互影響。

const source = { a : {b : 1} };
const objCopied = Object.assign({}, source);

console.log(objCopied); // { a: { b: 1 } }

source.a.b = 3;
console.log(source);  // { a: { b: 3 } }
console.log(objCopied);  // { a: { b: 3 } }

但是注意如下程式碼中,source.a = {};修改的是源物件中屬性的值,這個並不共用。

const source = { a : {b : 1} };
const objCopied = Object.assign({}, source);

console.log(objCopied); // { a: { b: 1 } }

source.a = {};
console.log(source);  // { a: {} }
console.log(objCopied);  // { a: { b: 1 } }

展開運運算元(...):展開語法(Spread syntax), 可以在函數呼叫/陣列構造時, 將陣列表示式或者string在語法層面展開;還可以在構造字面量物件時, 將物件表示式按key-value的方式展開。

const source = { a : {b : 1}, c: 2 };
const objCopied = {...source}

console.log(objCopied); // { a: { b: 1 }, c: 2 }

source.c = 3;
console.log(source);  // { a: { b: 1 }, c: 3 }
console.log(objCopied);  // { a: { b: 1 }, c: 2 }

source.a.b = 3;
console.log(source);  // { a: { b: 3 }, c: 3 }
console.log(objCopied);  // { a: { b: 3 }, c: 2 }

2.2 淺複製的手動實現

function shallowClone(source) {
    // 如果是原始值,直接返回
    if (typeof source !== 'object') {
        return source;
    }

    // 拷貝後的物件
    const copied = Array.isArray(source) ? [] : {};
    // 遍歷物件的key
    for(let key in source) {
        // 如果key是物件的自有屬性
        if(source.hasOwnProperty(key)) {
            // 複製屬性
            copied[key] = source[key]
        }
    }

    // 返回拷貝後的物件
    return copied;
}

三、深複製(Deep Copy)

首先來看深複製的定義:

A deep copy will duplicate every object it encounters. The copy and the original object will not share anything, so it will be a copy of the original.

與淺複製不同時,當源物件屬性的值為物件時,賦值的是該物件,而不是物件的參照。所以深複製中,源物件和拷貝物件之間不存在任何共用的內容。

2.1 原生 JavaScript 中的深複製

JSON.parse(JSON.stringify(object))

JavaScript 中最常見的深複製的方法就是JSON.parse(JSON.stringify(object))

如下程式碼所示,深複製中源物件和拷貝物件不共用任何內容,即使是巢狀物件。

let obj = {
    a: 1,
    b: {
        c: 2,
    },
}
let newObj = JSON.parse(JSON.stringify(obj));
obj.b.c = 20;
console.log(obj); // { a: 1, b: { c: 20 } }
console.log(newObj); // { a: 1, b: { c: 2 } }

2.2 深複製的手動實現

function deepClone(source) {
    // 如果是原始值,直接返回
    if (typeof source !== 'object') {
        return source;
    }
    // 拷貝後的物件
    const copied = Array.isArray(source) ? [] : {};
    // 遍歷物件的key
    for(let key in source) {
        // 如果key是物件的自有屬性
        if(source.hasOwnProperty(key)) {
            // 深複製
            copied[key] = deepClone(source[key]);
        }
    }
    return copied;
}

有關淺複製和深複製的手動實現,這裡只是簡單實現了一下。其中還有很多細節未實現,具體的實現大家可以參見 lodash 中的實現。

小結

  • 賦值操作符是把一個物件的參照賦值給一個變數,所以變數中儲存的是物件的參照
  • 淺複製是複製源物件的每個屬性,但如果屬性值是物件,那麼複製的是這個物件的參照。所以源物件和拷貝物件之間共用巢狀物件。
  • 深複製與淺複製不同的地方在於,如果屬性值為物件,那麼會複製該物件。源物件和拷貝物件之間不存在共用的內容。

到此這篇關於JavaScript 賦值,淺複製和深複製的區別的文章就介紹到這了,更多相關JS 賦值內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com