logo-yuki

Yuki

Post

とうこう

JavaScript 什麼是 by value 跟 by reference?

有這個觀念,就能解釋很多「我沒改它,但它變了」的現象。

5 分鐘
✦interview-preparation

你看,我剛才複製一個物件,改了新的,結果原本的物件也跟著被改⋯ JS 出 bug 啦!

才不是什麼 bug,只是 by reference 在搞鬼而已。

  • 這篇走輕鬆路線,是我自己消化完之後的筆記風格,不會寫得很長。
  • 想再深入的話,文中我會帶到關鍵字,文末也有推薦連結!

先來認識 JS 當中的資料型別

我想大家在讀 JS 書、準備面試時,多少都曾聽別人說過或在哪邊看過,相當老掉牙的這句話:

變數是一個存放資料的格子,格子裡面裝的可能是 value(值)、也可能是 reference(地址)。

裝得是 value 的呢,資料就是 primitive type;裝得是 reference 的呢,資料就是 reference type。

確實,在 JavaScript 的宇宙裡,資料分兩種型別:

Primitive Type

原始 / 基本型別都有人說。屬於這類型的資料,它本質是單一的值

stringnumberbooleannullundefinedsymbolbigint 都是。

Reference Type

引用 / 物件 型別也都有人說。屬於這類型的資料,它本質是一個資料集合體

諸如:objectarrayfunction

為什麼會這樣分?跟 JS 如何幫你儲存資料有關。

Web 前端的 JS 活在瀏覽器裡;後端(Node.js)的 JS 則直接跑在 OS 上。

不管前端後端,當你創建了一個變數,JS 就會跟電腦要一塊記憶體空間——也就是開一個格子。

primitive 的值體積小、大小固定,直接住進格子裡。

object / array / function 就不同了,結構複雜、大小不固定,沒辦法直接塞進格子。JS 的解法是:另找一塊更大的空間把東西安置好,然後把那個空間的地址放進格子裡。

格子裡存的是地址,不是物件本身——這就是 reference。

  • 關鍵字:stack(棧) and heap(堆)。

進入正題!

By Value

primitive type 的資料在賦值或傳參數的時候,複製的是值本身,兩個變數完全獨立。

js
let a = 10;
let b = a;
b = 99;

console.log(a); // 10 ← 不受影響
console.log(b); // 99

b 拿到的是 10 這個值的副本,跟 a 沒有任何關係。

函式的場合:

js
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 在賦值或傳參數的時候,複製的是記憶體位址,兩個變數指向同一塊記憶體。

js
const obj1 = { name: 'Yuzu' };
const obj2 = obj1;
obj2.name = 'Mikan';

console.log(obj1.name); // 'Mikan' ← 也被改了

obj2obj1 拿到的是同一個地址,改 obj2 就是在改同一塊記憶體裡的東西。

傳進函式也一樣:

js
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 早期的。但保留至今沒有修正,小彩蛋跟大家分享。

js
typeof null; // 'object' ← 歷史遺留問題,null 本身還是 primitive

為什麼不修?因為改了,整個 web 說不定會大壞掉——太多現有的程式碼依賴這個行為,向下相容性優先於正確性。估計以後也不會修了。

好文推薦

延伸閱讀