JavaScript 資料是怎麼儲存的?深入了解 Value Type 與 Reference Type
更新日期: 2025 年 4 月 19 日
你可能遇過這種狀況:
const a = [1, 2, 3];
const b = [1, 2, 3];
console.log(a === b); // false
看起來陣列內容一模一樣,為什麼卻不相等?
又或者:
const user = { name: "Amy" };
const users = [user];
user.name = "Bob";
console.log(users[0].name); // "Bob"
你沒有動到 users
陣列,但裡面的值卻被改變了?
這一切的背後,跟 JavaScript 如何儲存「原始資料」和「物件資料」有關,也就是:值類型(Value Type)與參考類型(Reference Type)。
資料類型大分類
JavaScript 將資料分成兩大類,每一類的儲存與處理方式都不一樣:
值類型(Value Types)— 存放「值」本身
包含:
string
number
boolean
null
undefined
symbol
bigint
這些值是「簡單且不可變」的,變數儲存的就是資料本體。
const a = 100;
const b = 100;
console.log(a === b); // true,因為內容一樣
參考類型(Reference Types)— 存放「指向記憶體位置的參考」
包含:
object
array
function
這些資料結構比較複雜,變數儲存的是「記憶體位置(Reference)」,也就是:資料本體其實存在別的地方。
const obj1 = { id: 1 };
const obj2 = { id: 1 };
console.log(obj1 === obj2); // false,即使內容看起來一樣
儲存邏輯差在哪?記憶體觀念圖解
類型 | 儲存在變數中的內容 | 比較時行為 | 是否共享資料 |
---|---|---|---|
值類型 | 值的本體 | 比較的是值 | ❌ 各自獨立 |
參考類型 | 記憶體參考(指標) | 比較的是記憶體位置 | ✅ 指向同一資料 |
📌 這就是為什麼:
[1, 2] === [1, 2]; // false,因為不同記憶體位置
flowchart LR subgraph 程式碼 A["let age = 30;"] B["let age1 = age;"] C["age = 31;"] D["let obj = { name: 'Tom' };"] E["let obj2 = obj;"] F["obj2.name = 'Jerry';"] end subgraph 記憶體儲存格 subgraph 值類型儲存區 Stack M1[("00XX1<br><i>記憶體位置編號</i>")] --- V1["30<br><i>儲存值</i>"] M2[("00XX2<br><i>記憶體位置編號</i>")] --- V2["31<br><i>儲存值</i>"] M3[("00XX3<br><i>記憶體位置編號</i>")] --- V3["30<br><i>儲存值</i>"] end subgraph 參考類型儲存區 Heap M4[("00YY1<br><i>變數記憶體位置</i>")] --- V4["FF01<br><i>儲存參考位址</i>"] M5[("00YY2<br><i>變數記憶體位置</i>")] --- V5["FF01<br><i>儲存參考位址</i>"] M6[("FF01<br><i>物件實際記憶體位置</i>")] --- V6["{name: 'Jerry'}<br><i>物件實際內容</i>"] end end A -->|"建立變數"| M1 B -->|"複製值"| M3 C -->|"修改值"| M2 D -->|"建立物件參考"| M4 E -->|"複製參考"| M5 V4 -->|"指向實際物件"| M6 V5 -->|"指向同一個物件"| M6 style M1 fill:#ddf8dd,stroke:#000000 style M2 fill:#ddf8dd,stroke:#000000 style M3 fill:#ddf8dd,stroke:#000000 style M4 fill:#fff0f0,stroke:#000000 style M5 fill:#fff0f0,stroke:#000000 style M6 fill:#fff0f0,stroke:#000000 style V1 fill:#ddf8dd,stroke:#000000 style V2 fill:#ddf8dd,stroke:#000000 style V3 fill:#ddf8dd,stroke:#000000 style V4 fill:#fff0f0,stroke:#000000,color:#008800 style V5 fill:#fff0f0,stroke:#000000,color:#008800 style V6 fill:#fff0f0,stroke:#000000
常見應用情境:比較、修改、複製資料的正確觀念
這一章節,我們會從實務常見的幾種操作出發,說明「值類型」和「參考類型」在 JavaScript 中的實際行為差異。
比較值 vs. 比較參考:看的是值,還是地址?
🧾 值類型的比較 —— 內容一致就相等
console.log(5 === 5); // true
console.log("hello" === "hello"); // true
- 值類型(例如
number
、string
)的比較是「看資料本身是否相等」。 - 兩邊的內容一樣,就會被視為相等。
🧠 想像成比對兩張紙條,兩張紙都寫著 “5”,你當然會說它們一樣。
🧾 參考類型的比較 —— 看的是記憶體位置
console.log([1, 2, 3] === [1, 2, 3]); // false
console.log({ name: "Tom" } === { name: "Tom" }); // false
- 即使資料內容「一模一樣」,JavaScript 也會判斷為 不同。
- 因為它們是兩個「不同記憶體位置」的資料。
🧠 就像兩份一模一樣的簡報檔案,但儲存在不同的 USB 隨身碟裡——你拿來比較時,是比「是哪個隨身碟」,不是「簡報長什麼樣」。
修改值 vs. 修改參考:哪裡在變?
🧾 修改值類型:變數之間彼此獨立
let scoreA = 80;
let scoreB = 80;
scoreA = 100;
console.log(scoreA); // 100
console.log(scoreB); // 80 ✅ 不受影響
🧠 比喻:就像你給了兩個人各自的紙本成績單
A 把自己的擦掉重寫成 100,B 的那份完全沒改變。
🧾 修改參考類型:共用資料,同步變動
const student = { name: "Mary" };
const studentA = student;
const studentB = student;
studentA.name = "John";
console.log(studentB.name); // "John" ❗️也變了
🧠 比喻:你把同一個 Google 文件連結給 A 和 B
A 把名字改了,B 一打開,也看到已經改成「John」的版本。
如何正確「複製」參考類型?
當你不希望物件之間互相影響(例如修改 A 不要影響 B),就必須「複製資料內容」而不是「複製參考」。
展開運算子(Shallow Copy)—— 適用於物件扁平結構
const original = { name: "David" };
const copy = { ...original };
copy.name = "Ethan";
console.log(original.name); // "David"
- 使用
{ ...物件 }
語法,可以複製第一層屬性的資料。 - 適用於「只有一層」的物件。
- 修改
copy
不會影響到original
,彼此獨立。
🧠 就像你影印了一張紙表單出來,你可以在影印本上改名字,不會動到原件。
展開運算子的限制:無法處理巢狀資料
const original = { info: { score: 100 } };
const copy = { ...original };
copy.info.score = 50;
console.log(original.info.score); // 50 ❗️會一起變
- 雖然第一層
info
是複製的,但info
的內容(score)還是指向同一記憶體位址。 - 這種情況下,兩者還是共享同一份「內部資料」。
深層複製(Deep Copy)—— 解決巢狀同步問題
const obj = { info: { score: 100 } };
const deepCopy = JSON.parse(JSON.stringify(obj));
deepCopy.info.score = 50;
console.log(obj.info.score); // 100 ✅ 不受影響
- 使用
JSON.parse(JSON.stringify(...))
可以將物件轉成字串再重建,達到深層複製效果。 - 適用於資料結構中沒有函式、
undefined
、Symbol
、Date
等特殊值時。
🧠 像是把一棟房子的所有東西都照著蓋一棟新的——包含家具、樓層結構、每一個房間的內容都重建一次。
延伸技巧:進階使用者可用工具函式庫
如果你需要更彈性、保留特殊資料型別(例如 Date
、Map
、Set
)的深層複製,推薦使用:
lodash.cloneDeep()
structuredClone()
(現代瀏覽器內建)
生活情境重新比喻:值 vs. 參考,就像「影本 vs. 雲端文件」
值類型(Value Type)就像「影印文件」
你請兩個同事來拿履歷資料:
const resumeA = "我是一份影印履歷";
const resumeB = "我是一份影印履歷";
- A 和 B 拿到的內容一模一樣,但彼此是兩張紙。
- A 把自己的紙塗鴉,不會影響 B 的那張。
- 系統比較這兩張時(
===
),看的是內容,只要一樣就算相等。
🔍 這就是值類型:變數儲存的是「資料本身」,獨立、純粹、可比。
參考類型(Reference Type)就像「共用 Google 文件的連結」
你不是列印紙,而是給兩人同一個 Google 文件的連結:
const doc = { content: "我是一份雲端履歷" };
const personA = doc;
const personB = doc;
- 他們連到同一份文件,改任何地方,另一人也會看到。
- 系統比較這兩個變數(
===
),會是true
,因為是同一個記憶體位置。
🔍 這是參考類型:變數存的是「資料的地址」,而不是資料本身。
❗️ 但如果你開了兩個長得一樣的 Google 文件(不同網址)
const docA = { content: "我是一份雲端履歷" };
const docB = { content: "我是一份雲端履歷" };
console.log(docA === docB); // false
- 即使他們的內容一模一樣,但網址不同,屬於兩份獨立文件。
- 改了其中一份,另一份不會變。
- 系統判斷時會說:這是兩個不同的物件(記憶體位置不同)。
🧠 換句話說:「你可以有兩份內容完全一樣的文件,但它們還是兩個不同的檔案。」
整體整理:值 vs. 參考的生活對照表
概念 | 值類型(Value) | 參考類型(Reference) |
---|---|---|
類比 | 紙本履歷(影印件) | 雲端文件(Google Docs) |
儲存方式 | 存放資料本身 | 存放文件網址(記憶體位置) |
比較依據 | 比對履歷內容 | 比對是否是「同一份文件」(同一個網址) |
修改行為 | 改一份不影響另一份 | 改了就會同步變動(共用同一份) |
相同內容時 | 視為相等(如果值一樣) | 還是視為不同(如果不是同一份物件) |
避免同步方式 | 每人發一份影印本 | 建立一個新 Google 文件,再傳新連結給另一人 |
為什麼學這個很重要?
當你進一步學習 React、Vue 或寫 API、管理狀態時,如果搞不清楚資料是值還是參考,可能會:
- 不小心改到不該動的資料(例如原本的 state)
- 寫出「看不見變動來源」的 bug
- 比較資料時一直得到 false,以為值不一樣
只要記住這個簡單口訣:
🔑 原始類型看內容,物件類型看記憶體位置。
總結與對照表
特性 | 值類型 | 參考類型 |
---|---|---|
儲存方式 | 直接儲存資料本體 | 儲存記憶體參考(指標) |
比較方式 | 比較資料內容 | 比較是否為同一個記憶體位置 |
複製後是否連動 | 不會 | 預設會連動 |
範例 | 10, "hi", true | {}, [], function(){} |