物件與基本型態之間的其中一個基本差異,在於物件是「透過參考」儲存和複製,而基本型態值:字串、數字、布林值等,則總是「以整體值」複製。
如果我們深入了解複製值時發生的情況,這一點很容易理解。
讓我們從基本型態開始,例如字串。
在這裡,我們將 message
的副本放入 phrase
let message = "Hello!";
let phrase = message;
結果,我們有兩個獨立的變數,每個變數都儲存著字串 "Hello!"
。
相當明顯的結果,對吧?
物件並非如此。
指定給物件的變數,儲存的並非物件本身,而是其「記憶體位址」—換句話說,就是對它的「參考」。
讓我們來看一個這樣的變數範例
let user = {
name: "John"
};
而這是它實際儲存在記憶體中的方式
物件儲存在記憶體中的某個地方(在圖片的右方),而 user
變數(在左方)有一個對它的「參考」。
我們可以將物件變數,例如 user
,想像成一張寫有物件地址的紙。
當我們對物件執行動作,例如取得屬性 user.name
,JavaScript 引擎會查看那個地址處的內容,並對實際物件執行操作。
現在來說明為什麼這很重要。
當複製一個物件變數時,會複製參考,但物件本身不會被複製。
例如
let user = { name: "John" };
let admin = user; // copy the reference
現在我們有兩個變數,每個變數都儲存對同一個物件的參考
如你所見,仍然只有一個物件,但現在有兩個變數參考它。
我們可以使用任一個變數來存取物件並修改其內容
let user = { name: 'John' };
let admin = user;
admin.name = 'Pete'; // changed by the "admin" reference
alert(user.name); // 'Pete', changes are seen from the "user" reference
這就像我們有一個有兩個鑰匙的櫃子,並使用其中一個(admin
)進入並進行變更。然後,如果我們稍後使用另一個鑰匙(user
),我們仍然會開啟同一個櫃子,並且可以存取已變更的內容。
透過參考進行比較
兩個物件只有在它們是同一個物件時才相等。
例如,這裡的 a
和 b
參考同一個物件,因此它們相等
let a = {};
let b = a; // copy the reference
alert( a == b ); // true, both variables reference the same object
alert( a === b ); // true
而這裡的兩個獨立物件不相等,即使它們看起來很像(都是空的)
let a = {};
let b = {}; // two independent objects
alert( a == b ); // false
對於像 obj1 > obj2
這樣的比較,或與基本型別 obj == 5
的比較,物件會轉換成基本型別。我們很快就會研究物件轉換是如何運作的,但老實說,這種比較很少需要用到 - 通常它們是程式設計錯誤的結果。
將物件儲存為參考的一個重要副作用是,宣告為 const
的物件可以被修改。
例如
const user = {
name: "John"
};
user.name = "Pete"; // (*)
alert(user.name); // Pete
(*)
這行看起來可能會造成錯誤,但它並不會。user
的值是常數,它必須始終參考同一個物件,但該物件的屬性可以自由變更。
換句話說,只有當我們嘗試將 user=...
設定為一個整體時,const user
才會產生錯誤。
話雖如此,如果我們真的需要建立常數物件屬性,也是可以的,但使用完全不同的方法。我們將在 屬性旗標和描述子 章節中提到這一點。
複製和合併,Object.assign
因此,複製一個物件變數會建立另一個對同一個物件的參考。
但是,如果我們需要複製一個物件怎麼辦?
我們可以建立一個新物件,並透過反覆運算其屬性並在原始層級複製它們,來複製現有物件的結構。
就像這樣
let user = {
name: "John",
age: 30
};
let clone = {}; // the new empty object
// let's copy all user properties into it
for (let key in user) {
clone[key] = user[key];
}
// now clone is a fully independent object with the same content
clone.name = "Pete"; // changed the data in it
alert( user.name ); // still John in the original object
我們也可以使用 Object.assign 方法。
語法為
Object.assign(dest, ...sources)
- 第一個參數
dest
是目標物件。 - 後續參數是來源物件清單。
它會將所有來源物件的屬性複製到目標 dest
中,然後回傳它作為結果。
例如,我們有一個 user
物件,讓我們新增幾個權限給它
let user = { name: "John" };
let permissions1 = { canView: true };
let permissions2 = { canEdit: true };
// copies all properties from permissions1 and permissions2 into user
Object.assign(user, permissions1, permissions2);
// now user = { name: "John", canView: true, canEdit: true }
alert(user.name); // John
alert(user.canView); // true
alert(user.canEdit); // true
如果已複製的屬性名稱已存在,它會被覆寫
let user = { name: "John" };
Object.assign(user, { name: "Pete" });
alert(user.name); // now user = { name: "Pete" }
我們也可以使用 Object.assign
來執行簡單的物件複製
let user = {
name: "John",
age: 30
};
let clone = Object.assign({}, user);
alert(clone.name); // John
alert(clone.age); // 30
這裡它會將 user
的所有屬性複製到空物件中,並回傳它。
還有其他複製物件的方法,例如使用 展開語法 clone = {...user}
,會在後面的教學中介紹。
巢狀複製
到目前為止,我們假設 user
的所有屬性都是原始值。但屬性可以是其他物件的參考。
就像這樣
let user = {
name: "John",
sizes: {
height: 182,
width: 50
}
};
alert( user.sizes.height ); // 182
現在複製 clone.sizes = user.sizes
不夠了,因為 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, same object
// user and clone share sizes
user.sizes.width = 60; // change a property from one place
alert(clone.sizes.width); // 60, get the result from the other one
要修正這個問題,並讓 user
和 clone
真正成為獨立的物件,我們應該使用複製迴圈來檢查 user[key]
的每個值,如果它是個物件,則複製它的結構。這稱為「深度複製」或「結構化複製」。有一個 structuredClone 方法可以執行深度複製。
structuredClone
呼叫 structuredClone(object)
會複製 object
和所有巢狀屬性。
以下是我們如何在範例中使用它
let user = {
name: "John",
sizes: {
height: 182,
width: 50
}
};
let clone = structuredClone(user);
alert( user.sizes === clone.sizes ); // false, different objects
// user and clone are totally unrelated now
user.sizes.width = 60; // change a property from one place
alert(clone.sizes.width); // 50, not related
structuredClone
方法可以複製大多數資料類型,例如物件、陣列、原始值。
它也支援循環參考,也就是當物件屬性參考物件本身(直接或透過參考鏈)時。
例如
let user = {};
// let's create a circular reference:
// user.me references the user itself
user.me = user;
let clone = structuredClone(user);
alert(clone.me === clone); // true
如你所見,clone.me
參考 clone
,而不是 user
!所以循環參考也正確地複製了。
儘管如此,還是有 structuredClone
會失敗的情況。
例如,當物件有一個函式屬性時
// error
structuredClone({
f: function() {}
});
函式屬性不受支援。
要處理這種複雜情況,我們可能需要使用複製方法的組合、撰寫自訂程式碼,或是不重覆造輪子,採用現有的實作,例如 JavaScript 函式庫 lodash 中的 _.cloneDeep(obj)。
摘要
物件會透過參考指派和複製。換句話說,變數儲存的不是「物件值」,而是該值的「參考」(記憶體中的位址)。所以複製這樣的變數或將它作為函式參數傳遞,會複製該參考,而不是物件本身。
透過複製參考的所有操作(例如新增/移除屬性)都對同一個物件執行。
要建立「真實的複製」(複製)我們可以使用 Object.assign
來進行所謂的「淺層複製」(巢狀物件會透過參考複製)或「深度複製」函式 structuredClone
,或使用自訂複製實作,例如 _.cloneDeep(obj)。
留言
<code>
標籤,要插入多行程式碼 - 請用<pre>
標籤包起來,要插入超過 10 行程式碼 - 請使用沙盒 (plnkr、jsbin、codepen…)