你看,我剛才複製一個物件,改了新的,結果原本的物件也跟著被改⋯ JS 出 bug 啦!
才不是什麼 bug,只是 by reference 在搞鬼而已。
- 這篇走輕鬆路線,是我自己消化完之後的筆記風格,不會寫得很長。
- 想再深入的話,文中我會帶到關鍵字,文末也有推薦連結!
先來認識 JS 當中的資料型別
我想大家在讀 JS 書、準備面試時,多少都曾聽別人說過或在哪邊看過,相當老掉牙的這句話:
變數是一個存放資料的格子,格子裡面裝的可能是 value(值)、也可能是 reference(地址)。
裝得是 value 的呢,資料就是 primitive type;裝得是 reference 的呢,資料就是 reference type。
確實,在 JavaScript 的宇宙裡,資料分兩種型別:
Primitive Type
原始 / 基本型別都有人說。屬於這類型的資料,它本質是單一的值。
string、number、boolean、null、undefined、symbol、bigint 都是。
Reference Type
引用 / 物件 型別也都有人說。屬於這類型的資料,它本質是一個資料集合體。
諸如:object、array、function。
為什麼會這樣分?跟 JS 如何幫你儲存資料有關。
Web 前端的 JS 活在瀏覽器裡;後端(Node.js)的 JS 則直接跑在 OS 上。
不管前端後端,當你創建了一個變數,JS 就會跟電腦要一塊記憶體空間——也就是開一個格子。
primitive 的值體積小、大小固定,直接住進格子裡。
object / array / function 就不同了,結構複雜、大小不固定,沒辦法直接塞進格子。JS 的解法是:另找一塊更大的空間把東西安置好,然後把那個空間的地址放進格子裡。
格子裡存的是地址,不是物件本身——這就是 reference。
- 關鍵字:stack(棧) and heap(堆)。
進入正題!
By Value
primitive type 的資料在賦值或傳參數的時候,複製的是值本身,兩個變數完全獨立。
let a = 10;
let b = a;
b = 99;
console.log(a); // 10 ← 不受影響
console.log(b); // 99b 拿到的是 10 這個值的副本,跟 a 沒有任何關係。
函式的場合:
function double(n) {
n = n * 2;
console.log(n); // 20
}
let a = 10;
double(a);
console.log(a); // 10 ← 不受影響,n 只是 a 值的副本By Reference
object 和 array 在賦值或傳參數的時候,複製的是記憶體位址,兩個變數指向同一塊記憶體。
const obj1 = { name: 'Yuzu' };
const obj2 = obj1;
obj2.name = 'Mikan';
console.log(obj1.name); // 'Mikan' ← 也被改了obj2 跟 obj1 拿到的是同一個地址,改 obj2 就是在改同一塊記憶體裡的東西。
傳進函式也一樣:
function rename(obj) {
obj.name = 'Mikan';
}
const user = { name: 'Yuzu' };
rename(user);
console.log(user.name); // 'Mikan' ← 函式外面也被改了這不是 bug,是 by reference 的本質。
總結
所以,下次遇到我沒改它,但它變了的情況,心裡應該就有個底,知道可以去哪裡找答案了吧!
加映:typeof null === 'object'
null 是 Primitive,但 typeof null 回傳的卻會是 'object'。
這就真的是 bug 啦! JS 早期的。但保留至今沒有修正,小彩蛋跟大家分享。
typeof null; // 'object' ← 歷史遺留問題,null 本身還是 primitive為什麼不修?因為改了,整個 web 說不定會大壞掉——太多現有的程式碼依賴這個行為,向下相容性優先於正確性。估計以後也不會修了。
好文推薦
- Object references and copying — javascript.info
- JS 變數傳遞探討:pass by value 、 pass by reference 還是 pass by sharing?