[Medium] 📄 Primitive vs Reference Types
1. What are Primitive Types and Reference Types?
什麼是原始型別(Primitive Types)和參考型別(Reference Types)?
JavaScript 的資料型別分為兩大類:原始型別和參考型別。它們在記憶體儲存方式和傳遞行為上有本質的差異。
原始型別(Primitive Types)
特點:
- 儲存在**堆疊(Stack)**中
- 傳遞時複製值本身(Call by Value)
- 不可變的(Immutable)
包含 7 種:
// 1. String(字串)
const str = 'hello';
// 2. Number(數字)
const num = 42;
// 3. Boolean(布林值)
const bool = true;
// 4. Undefined
let undef;
// 5. Null
const n = null;
// 6. Symbol(ES6)
const sym = Symbol('unique');
// 7. BigInt(ES2020)
const bigInt = 9007199254740991n;
參考型別(Reference Types)
特點:
- 儲存在**堆積(Heap)**中
- 傳遞時複製參考(記憶體地址)(Call by Reference)
- 可變的(Mutable)
包含:
// 1. Object(物件)
const obj = { name: 'John' };
// 2. Array(陣列)
const arr = [1, 2, 3];
// 3. Function(函式)
const func = function () {};
// 4. Date
const date = new Date();
// 5. RegExp
const regex = /abc/;
// 6. Map, Set, WeakMap, WeakSet (ES6)
const map = new Map();
const set = new Set();
2. Call by Value vs Call by Reference
傳值(Call by Value)vs 傳址(Call by Reference)
傳值(Call by Value)- 原始型別
行為:複製值本身,修改副本不影響原值。
// 原始型別:傳值
let a = 10;
let b = a; // 複製值
b = 20; // 修改 b
console.log(a); // 10(不受影響)
console.log(b); // 20
記憶體示意圖:
┌─────────┐
│ Stack │
├─────────┤
│ a: 10 │ ← 獨立的值
├─────────┤
│ b: 20 │ ← 獨立的值(複製後修改)
└─────────┘
傳址(Call by Reference)- 參考型別
行為:複製記憶體地址,兩個變數指向同一個物件。
// 參考型別:傳址
let obj1 = { name: 'John' };
let obj2 = obj1; // 複製記憶體地址
obj2.name = 'Jane'; // 透過 obj2 修改
console.log(obj1.name); // 'Jane'(被影響!)
console.log(obj2.name); // 'Jane'
console.log(obj1 === obj2); // true(指向同一個物件)
記憶體示意圖:
┌─────────┐ ┌──────────────────┐
│ Stack │ │ Heap │
├─────────┤ ├──────────────────┤
│ obj1 ───┼───────────────────>│ { name: 'Jane' } │
├─────────┤ │ │
│ obj2 ───┼───────────────────>│ (同一個物件) │
└─────────┘ └──────────────────┘
3. Common Quiz Questions
常見測驗題
題目 1:原始型別的傳遞
function changeValue(x) {
x = 100;
console.log('函式內 x:', x);
}
let num = 50;
changeValue(num);
console.log('函式外 num:', num);
點擊查看答案
// 函式內 x: 100
// 函式外 num: 50
解釋:
num是原始型別(Number)- 傳入函式時複製值,
x和num是兩個獨立的變數 - 修改
x不會影響num
// 執行流程
let num = 50; // Stack: num = 50
changeValue(num); // Stack: x = 50(複製)
x = 100; // Stack: x = 100(只修改 x)
console.log(num); // Stack: num = 50(不受影響)
題目 2:參考型別的傳遞
function changeObject(obj) {
obj.name = 'Changed';
console.log('函式內 obj.name:', obj.name);
}
let person = { name: 'Original' };
changeObject(person);
console.log('函式外 person.name:', person.name);
點擊查看答案
// 函式內 obj.name: Changed
// 函式外 person.name: Changed
解釋:
person是參考型別(Object)- 傳入函式時複製記憶體地址
obj和person指向同一個物件- 透過
obj修改物件內容,person也會被影響
// 記憶體示意
let person = { name: 'Original' }; // Heap: 建立物件 @0x001
changeObject(person); // Stack: obj = @0x001(複製地址)
obj.name = 'Changed'; // Heap: @0x001.name = 'Changed'
console.log(person.name); // Heap: @0x001.name(同一個物件)
題目 3:重新賦值 vs 修改屬性
function test1(obj) {
obj.name = 'Modified'; // 修改屬性
}
function test2(obj) {
obj = { name: 'New Object' }; // 重新賦值
}
let person = { name: 'Original' };
test1(person);
console.log('A:', person.name);
test2(person);
console.log('B:', person.name);
點擊查看答案
// A: Modified
// B: Modified(不是 'New Object'!)
解釋:
test1:修改屬性
function test1(obj) {
obj.name = 'Modified'; // ✅ 修改原物件的屬性
}
// person 和 obj 指向同一個物件,所以會被修改
test2:重新賦值
function test2(obj) {
obj = { name: 'New Object' }; // ❌ 只改變 obj 的指向
}
// obj 現在指向新物件,但 person 仍指向原物件
記憶體示意圖:
// test1 之前
person ────> { name: 'Original' }
obj ────> { name: 'Original' } (同一個)
// test1 之後
person ────> { name: 'Modified' }
obj ────> { name: 'Modified' } (同一個)
// test2 執行
person ────> { name: 'Modified' } (不變)
obj ────> { name: 'New Object' } (新物件)
// test2 結束後
person ────> { name: 'Modified' } (仍然不變)
// obj 被銷毀,新物件被垃圾回收
題目 4:陣列的傳遞
function modifyArray(arr) {
arr.push(4);
console.log('1:', arr);
}
function reassignArray(arr) {
arr = [5, 6, 7];
console.log('2:', arr);
}
let numbers = [1, 2, 3];
modifyArray(numbers);
console.log('3:', numbers);
reassignArray(numbers);
console.log('4:', numbers);
點擊查看答案
// 1: [1, 2, 3, 4]
// 3: [1, 2, 3, 4]
// 2: [5, 6, 7]
// 4: [1, 2, 3, 4]
解釋:
modifyArray:修改原陣列內容,numbers被影響reassignArray:只改變參數的指向,numbers不受影響
題目 5:比較運算
// 原始型別比較
let a = 10;
let b = 10;
console.log('A:', a === b);
// 參考型別比較
let obj1 = { value: 10 };
let obj2 = { value: 10 };
let obj3 = obj1;
console.log('B:', obj1 === obj2);
console.log('C:', obj1 === obj3);
點擊查看答案
// A: true
// B: false
// C: true
解釋:
原始型別:比較值
10 === 10; // true(值相同)
參考型別:比較記憶體地址
obj1 === obj2; // false(不同物件,不同地址)
obj1 === obj3; // true(指向同一個物件)
記憶體示意:
obj1 ────> @0x001: { value: 10 }
obj2 ────> @0x002: { value: 10 } (內容相同但地址不同)
obj3 ────> @0x001: { value: 10 } (與 obj1 相同地址)
4. Shallow Copy vs Deep Copy
淺拷貝 vs 深拷貝
淺拷貝(Shallow Copy)
定義:只複製第一層,巢狀物件仍然是參考。
方法 1:展開運算子(Spread Operator)
const original = {
name: 'John',
address: { city: 'Taipei' },
};
const copy = { ...original };
// 修改第一層:不影響原物件
copy.name = 'Jane';
console.log(original.name); // 'John'(不受影響)
// 修改巢狀物件:影響原物件!
copy.address.city = 'Kaohsiung';
console.log(original.address.city); // 'Kaohsiung'(被影響!)
方法 2:Object.assign()
const original = { name: 'John', age: 30 };
const copy = Object.assign({}, original);
copy.name = 'Jane';
console.log(original.name); // 'John'(不受影響)
方法 3:陣列的淺拷貝
const arr1 = [1, 2, 3];
// 方法 1:展開運算子
const arr2 = [...arr1];
// 方法 2:slice()
const arr3 = arr1.slice();
// 方法 3:Array.from()
const arr4 = Array.from(arr1);
arr2[0] = 999;
console.log(arr1[0]); // 1(不受影響)
深拷貝(Deep Copy)
定義:完全複製所有層級,包括巢狀物件。
方法 1:JSON.parse + JSON.stringify(最常用)
const original = {
name: 'John',
address: { city: 'Taipei' },
hobbies: ['reading', 'gaming'],
};
const copy = JSON.parse(JSON.stringify(original));
// 修改巢狀物件:不影響原物件
copy.address.city = 'Kaohsiung';
console.log(original.address.city); // 'Taipei'(不受影響)
copy.hobbies.push('coding');
console.log(original.hobbies); // ['reading', 'gaming'](不受影響)
限制:
const obj = {
date: new Date(), // ❌ 會變成字串
func: () => {}, // ❌ 會被忽略
undef: undefined, // ❌ 會被忽略
symbol: Symbol('test'), // ❌ 會被忽略
regexp: /abc/, // ❌ 會變成 {}
circular: null, // ❌ 循環參考會報錯
};
obj.circular = obj; // 循環參考
JSON.parse(JSON.stringify(obj)); // 會出錯或遺失資料
方法 2:structuredClone()(現代瀏覽器)
const original = {
name: 'John',
address: { city: 'Taipei' },
date: new Date(),
};
const copy = structuredClone(original);
// 可以正確複製 Date 等特殊物件
console.log(copy.date instanceof Date); // true
優點:
- ✅ 支援 Date、RegExp、Map、Set 等
- ✅ 支援循環參考
- ✅ 效能較好
限制:
- ❌ 不支援函式
- ❌ 不支援 Symbol