JavaScript 物件可以放入字串、數字、布林值等 primitive data,同時也可以放入更多物件、陣列、函式等等。前述的靜態資料在物件當中稱為屬性 (property),而函示則稱為方法 (method)。
函示在 JavaScript 裡面通常會以匿名函式的樣態呈現,以下提供程式碼參考:
let user = {
name: "Mimiball",
age: 30,
greet: function () {
console.log("Hey!")
}
};
user.greet(); // Hello!
這樣運作下來其實和一般的函示沒有太大差異,但如果我們希望藉由物件中的函示,來存取其他同個物件當中的資料,就需要注意 this
關鍵字了。
假設我們希望 greet 這個函式除了列印出純字串之外,也想帶入物件屬性 name 的值,可以參考以下寫法:
let user = {
name: "Mimiball",
age: 30,
greet: function () {
console.log(`Hey! This is ${user.name}`);
}
};
user.greet(); // Hey! This is Mimiball
我們透過 Template Literals 將 name 帶入到 console.log
裡面,而 name 既然是物件 user 的屬性,所以要用點記法 (dot notation) 來取出屬性的值。
嗯~事情看起來非常順利,但這樣的寫法其實不可靠。我們將狀況搞得複雜一些,把 user 「複製」給另一個變數 admin,然後再白目地賦值 null
給 user,最後呼叫 admin.greet()
:
let user = {
name: "Mimiball",
age: 30,
greet: function () {
console.log(`Hey! This is ${user.name}`);
}
};
let admin = user;
user = null;
admin.greet(); // TypeError: Cannot read property 'name' of null
Cannot read property 'name' of null... 但我明明是在 user 複製給 admin 之後才將 user 重新賦值 null
啊 🫠
抱著疑惑的心,我開始上網輸入 JavaScript object copy 等關鍵字才恍然大悟。JavaScript 的物件是 by reference!
由於網路上有太多相關的精采文章了,這邊不再花篇幅紀錄。總而言之,JavaScript 的物件是 by reference,更嚴格來說似乎是 by sharing,但大家還是習慣以 by reference 稱呼之。
簡單來說,by reference 就是複製了 user 物件的記憶體位址 (memory address) 給 admin,所以 user 和 admin 其實都指向同一個物件。難怪 user 被覆寫成 null
之後,admin.greet()
會顯示無法讀取 null
的屬性。
既然無法用 user.name
來取值,那個怎麼辦呢?問題總是要解決吧?這個時候關鍵字 this 就派上用場了。
this
關鍵字很特殊,它代表了當前執行程式碼的物件,所以在不同的情境之下,this
的值可能發生變化。由於我們這次遇到的問題和 method 有關,所以只先看函式中的 this
。
在函式中,this
的值取決於函式是如何被呼叫的。如果函數是作為物件的 method 被呼叫,那this
就指向呼叫該 method 的物件。
更加白話文的版本:
this
的值是「.
之前的物件」,也就是用來呼叫該方法的那個物件。
回到前面的問題,我們把程式碼改成 this.name
:
let user = {
name: "Mimiball",
age: 30,
greet: function () {
console.log(`Hey! This is ${this.name}`); // Modify this line of code
}
};
let admin = user;
user = null;
admin.greet(); // Hey! This is mimiball
媽媽咪呀 🤌 Perfecto! 問題解決了!
等等,可是這樣還是有點奇怪啊,admin 呼叫了 this,可是剛剛又說物件是 by reference,那就算改成 this.name
,還是指向已經被覆寫為 null
的物件了不是嗎?這樣 user.name
改成 this.name
等於換湯不換藥唉......
腦中浮現了這樣的疑問,我才意識到自己根本沒搞懂前面 by reference 背後的記憶體分配概念,好在 Alpha Camp 助教以及 ChatGPT 的協助下,我終於看清楚了背後的原因。
undefined
,直到我們將物件腹值給 user,但由於 JavaScript 物件是 by reference,所以實際上儲存在變數 user 的值為物件的記憶體位址 0x0001。其實問題裡面的 let admin = user
並不是很好的物件複製方式。從上面第二張圖可以發現,利用這樣將 a 物件賦值給 b 物件的手法,會造成之後無論透過誰來更改物件屬性都彼此影響,因為兩者指向的始終為同一個物件。
有鑑於此,我們能善用 spread operator
、Object.assign()
等方法進行物件複製。值得留意的是,如果物件裡面有第二層物件或陣列 (陣列在 JavaScript 也是物件......),spread operator
在複製第二層時,還是會回到 by reference。這似乎與淺拷貝 (shallow copy) 有關,這部分可以參考下方的文章連結~