Javascript 對象引用和復制

2023-02-17 10:38 更新

對象與原始類型的根本區(qū)別之一是,對象是“通過引用”存儲和復制的,而原始類型:字符串、數(shù)字、布爾值等 —— 總是“作為一個整體”復制。

如果我們深入了解復制值時會發(fā)生什么,就很容易理解了。

讓我們從原始類型開始,例如一個字符串。

這里我們將 message 復制到 phrase

let message = "Hello!";
let phrase = message;

結果我們就有了兩個獨立的變量,每個都存儲著字符串 "Hello!"。


顯而易見的結果,對吧?

但是,對象不是這樣的。

賦值了對象的變量存儲的不是對象本身,而是該對象“在內(nèi)存中的地址” —— 換句話說就是對該對象的“引用”。

讓我們看一個這樣的變量的例子:

let user = {
  name: "John"
};

這是它實際存儲在內(nèi)存中的方式:


該對象被存儲在內(nèi)存中的某個位置(在圖片的右側),而變量 user(在左側)保存的是對其的“引用”。

我們可以將一個對象變量(例如 user)想象成一張寫有對象的地址的紙。

當我們對對象執(zhí)行操作時,例如獲取一個屬性 user.name,JavaScript 引擎會查看該地址中的內(nèi)容,并在實際對象上執(zhí)行操作。

現(xiàn)在,這就是為什么它很重要。

當一個對象變量被復制 —— 引用被復制,而該對象自身并沒有被復制。

例如:

let user = { name: "John" };

let admin = user; // 復制引用

現(xiàn)在我們有了兩個變量,它們保存的都是對同一個對象的引用:


正如你所看到的,這里仍然只有一個對象,但現(xiàn)在有兩個引用它的變量。

我們可以通過其中任意一個變量來訪問該對象并修改它的內(nèi)容:

let user = { name: 'John' };

let admin = user;

admin.name = 'Pete'; // 通過 "admin" 引用來修改

alert(user.name); // 'Pete',修改能通過 "user" 引用看到

這就像我們有一個帶有兩把鑰匙的柜子,使用其中一把鑰匙(admin)打開柜子并更改了里面的東西。那么,如果我們稍后用另一把鑰匙(user),我們?nèi)匀豢梢源蜷_同一個柜子并且可以訪問更改的內(nèi)容。

通過引用來比較

僅當兩個對象為同一對象時,兩者才相等。

例如,這里 a 和 b 兩個變量都引用同一個對象,所以它們相等:

let a = {};
let b = a; // 復制引用

alert( a == b ); // true,都引用同一對象
alert( a === b ); // true

而這里兩個獨立的對象則并不相等,即使它們看起來很像(都為空):

let a = {};
let b = {}; // 兩個獨立的對象

alert( a == b ); // false

對于類似 obj1 > obj2 的比較,或者跟一個原始類型值的比較 obj == 5,對象都會被轉換為原始值。我們很快就會學到對象是如何轉換的,但是說實話,很少需要進行這樣的比較 —— 通常是在編程錯誤的時候才會出現(xiàn)這種情況。

克隆與合并,Object.assign

那么,拷貝一個對象變量會又創(chuàng)建一個對相同對象的引用。

但是,如果我們想要復制一個對象,那該怎么做呢?

我們可以創(chuàng)建一個新對象,通過遍歷已有對象的屬性,并在原始類型值的層面復制它們,以實現(xiàn)對已有對象結構的復制。

就像這樣:

let user = {
  name: "John",
  age: 30
};

let clone = {}; // 新的空對象

// 將 user 中所有的屬性拷貝到其中
for (let key in user) {
  clone[key] = user[key];
}

// 現(xiàn)在 clone 是帶有相同內(nèi)容的完全獨立的對象
clone.name = "Pete"; // 改變了其中的數(shù)據(jù)

alert( user.name ); // 原來的對象中的 name 屬性依然是 John

我們也可以使用 Object.assign 方法來達成同樣的效果。

語法是:

Object.assign(dest, [src1, src2, src3...])
  • 第一個參數(shù) ?dest ?是指目標對象。
  • 更后面的參數(shù) ?src1, ..., srcN?(可按需傳遞多個參數(shù))是源對象。
  • 該方法將所有源對象的屬性拷貝到目標對象 ?dest ?中。換句話說,從第二個開始的所有參數(shù)的屬性都被拷貝到第一個參數(shù)的對象中。
  • 調(diào)用結果返回 ?dest?。

例如,我們可以用它來合并多個對象:

let user = { name: "John" };

let permissions1 = { canView: true };
let permissions2 = { canEdit: true };

// 將 permissions1 和 permissions2 中的所有屬性都拷貝到 user 中
Object.assign(user, permissions1, permissions2);

// 現(xiàn)在 user = { name: "John", canView: true, canEdit: true }

如果被拷貝的屬性的屬性名已經(jīng)存在,那么它會被覆蓋:

let user = { name: "John" };

Object.assign(user, { name: "Pete" });

alert(user.name); // 現(xiàn)在 user = { name: "Pete" }

我們也可以用 Object.assign 代替 for..in 循環(huán)來進行簡單克?。?

let user = {
  name: "John",
  age: 30
};

let clone = Object.assign({}, user);

它將 user 中的所有屬性拷貝到了一個空對象中,并返回這個新的對象。

還有其他克隆對象的方法,例如使用 spread 語法 clone = {...user},在后面的章節(jié)中我們會講到。

深層克隆

到現(xiàn)在為止,我們都假設 user 的所有屬性均為原始類型。但屬性可以是對其他對象的引用。

例如:

let user = {
  name: "John",
  sizes: {
    height: 182,
    width: 50
  }
};

alert( user.sizes.height ); // 182

現(xiàn)在這樣拷貝 clone.sizes = user.sizes 已經(jīng)不足夠了,因為 user.sizes 是個對象,它會以引用形式被拷貝。因此 clone 和 user 會共用一個 sizes:

let user = {
  name: "John",
  sizes: {
    height: 182,
    width: 50
  }
};

let clone = Object.assign({}, user);

alert( user.sizes === clone.sizes ); // true,同一個對象

// user 和 clone 分享同一個 sizes
user.sizes.width++;       // 通過其中一個改變屬性值
alert(clone.sizes.width); // 51,能從另外一個獲取到變更后的結果

為了解決這個問題,并讓 user 和 clone 成為兩個真正獨立的對象,我們應該使用一個拷貝循環(huán)來檢查 user[key] 的每個值,如果它是一個對象,那么也復制它的結構。這就是所謂的“深拷貝”。

我們可以使用遞歸來實現(xiàn)它。或者為了不重復造輪子,采用現(xiàn)有的實現(xiàn),例如 lodash 庫的 _.cloneDeep(obj)。

使用 const 聲明的對象也是可以被修改的

通過引用對對象進行存儲的一個重要的副作用是聲明為 const 的對象 可以 被修改。

例如:

const user = {
  name: "John"
};

user.name = "Pete"; // (*)

alert(user.name); // Pete

看起來 (*) 行的代碼會觸發(fā)一個錯誤,但實際并沒有。user 的值是一個常量,它必須始終引用同一個對象,但該對象的屬性可以被自由修改。

換句話說,只有當我們嘗試將 user=... 作為一個整體進行賦值時,const user 才會報錯。

也就是說,如果我們真的需要創(chuàng)建常量對象屬性,也是可以的,但使用的是完全不同的方法。我們將在 屬性標志和屬性描述符 一章中學習它。

總結

對象通過引用被賦值和拷貝。換句話說,一個變量存儲的不是“對象的值”,而是一個對值的“引用”(內(nèi)存地址)。因此,拷貝此類變量或?qū)⑵渥鳛楹瘮?shù)參數(shù)傳遞時,所拷貝的是引用,而不是對象本身。

所有通過被拷貝的引用的操作(如添加、刪除屬性)都作用在同一個對象上。

為了創(chuàng)建“真正的拷貝”(一個克?。?,我們可以使用 Object.assign 來做所謂的“淺拷貝”(嵌套對象被通過引用進行拷貝)或者使用“深拷貝”函數(shù),例如 _.cloneDeep(obj)。


以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號