物件與 JSON#
物件(Object)是由一組 key-value 對組成的資料結構,用來表示具有多個屬性的事物。
JavaScript 中幾乎所有東西都是物件,理解物件的操作方式是掌握這門語言的核心。
1
2
3
4
5
6
| // 物件字面值(最常用的建立方式)
const user = {
name: "Alice", // key: "name", value: "Alice"
age: 25,
isStudent: false
};
|
存取屬性#
有兩種存取方式:
1
2
3
4
5
6
7
8
| // 點記法(dot notation):最常用,key 必須是合法的識別字
console.log(user.name); // "Alice"
// 括號記法(bracket notation):key 可以是變數或含特殊字元的字串
console.log(user["age"]); // 25
const key = "name";
console.log(user[key]); // "Alice"(適合動態存取)
|
新增、修改、刪除#
1
2
3
4
5
6
7
| user.email = "alice@example.com"; // 新增屬性
user.age = 26; // 修改屬性
delete user.isStudent; // 刪除屬性
// 判斷屬性是否存在
console.log("name" in user); // true
console.log("phone" in user); // false
|
方法(Methods)#
物件的屬性值可以是函式,這種屬性稱為方法。方法內可用 this 存取物件本身的屬性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| const calc = {
value: 0,
add(n) {
this.value += n;
return this; // 回傳 this,讓方法可以鏈式呼叫
},
subtract(n) {
this.value -= n;
return this;
},
result() {
return this.value;
}
};
// 鏈式呼叫:每個方法回傳 this,所以可以連續呼叫
console.log(calc.add(10).add(5).subtract(3).result()); // 12
|
物件解構#
解構賦值讓你可以從物件中快速取出屬性,賦值給同名的變數:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| const user = { name: "Bob", age: 30, city: "台北" };
// 基本解構:變數名稱必須與 key 相同
const { name, age } = user;
console.log(name, age); // Bob 30
// 重新命名:把 name 存到 userName,city 存到 location
const { name: userName, city: location } = user;
console.log(userName, location); // Bob 台北
// 預設值:若屬性不存在,使用預設值
const { role = "user" } = user; // user 沒有 role,使用 "user"
console.log(role); // "user"
// 重新命名 + 預設值
const { phone: tel = "未填寫" } = user;
console.log(tel); // "未填寫"
|
函式參數解構#
直接在參數位置解構,讓呼叫端傳物件、函式內直接用屬性:
1
2
3
4
5
6
7
8
9
10
11
| // 不用解構:需先取出屬性
function greet(user) {
return `${user.name} 今年 ${user.age} 歲`;
}
// 用解構:更簡潔
function greet({ name, age }) {
return `${name} 今年 ${age} 歲`;
}
console.log(greet(user)); // Bob 今年 30 歲
|
展開與合併物件#
... 展開運算子可以將物件的所有屬性展開到另一個物件,後面的屬性會覆蓋前面相同 key 的屬性:
1
2
3
4
5
6
7
| const defaults = { color: "blue", size: "M", quantity: 1 };
const custom = { color: "red", quantity: 3 };
const order = { ...defaults, ...custom };
console.log(order);
// { color: "red", size: "M", quantity: 3 }
// color 和 quantity 被 custom 覆蓋,size 保留 defaults 的值
|
常見用途:
1
2
3
4
5
6
7
8
| // 複製物件(淺拷貝)
const copy = { ...user };
// 在複製時新增或覆蓋某個屬性
const updatedUser = { ...user, age: 31 };
// 合併多個設定
const config = { ...defaultConfig, ...userConfig, ...envConfig };
|
Object 靜態方法#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| const user = { name: "Alice", age: 25, city: "台北" };
// Object.keys — 取得所有 key 組成的陣列
console.log(Object.keys(user));
// ["name", "age", "city"]
// Object.values — 取得所有 value 組成的陣列
console.log(Object.values(user));
// ["Alice", 25, "台北"]
// Object.entries — 取得所有 [key, value] 對組成的陣列
console.log(Object.entries(user));
// [["name", "Alice"], ["age", 25], ["city", "台北"]]
// Object.fromEntries — 從 [key, value] 陣列建立物件(entries 的反向操作)
const entries = [["a", 1], ["b", 2]];
console.log(Object.fromEntries(entries)); // { a: 1, b: 2 }
|
遍歷物件#
物件不像陣列有 forEach,需要透過以下方式遍歷:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| const scores = { Alice: 90, Bob: 85, Carol: 92 };
// for...in:遍歷所有 key
for (const name in scores) {
console.log(`${name}: ${scores[name]}`);
}
// Alice: 90
// Bob: 85
// Carol: 92
// Object.entries + forEach:同時取得 key 和 value(推薦)
Object.entries(scores).forEach(([name, score]) => {
console.log(`${name}: ${score}`);
});
// 搭配陣列方法做計算,例如找出最高分
const [topName, topScore] = Object.entries(scores)
.reduce((max, curr) => curr[1] > max[1] ? curr : max);
console.log(`最高分:${topName} ${topScore}`); // 最高分:Carol 92
|
選擇性鏈結(Optional Chaining)#
存取深層屬性時,若中間某層是 null 或 undefined,直接用 . 存取會拋出錯誤:
1
2
3
| const user = { profile: null };
console.log(user.profile.address); // TypeError: Cannot read properties of null
|
傳統的防禦寫法是用 && 逐層檢查,但很冗長:
1
| const city = user.profile && user.profile.address && user.profile.address.city;
|
ES2020 引入的 ?. 運算子(選擇性鏈結)可以簡化這個流程:遇到 null 或 undefined 時立即停止,回傳 undefined,不拋出錯誤。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| const user = {
profile: {
address: {
city: "台北"
}
}
};
// 屬性存取
const city = user?.profile?.address?.city;
console.log(city); // "台北"
// 中間層不存在時,安全地回傳 undefined
const user2 = { profile: null };
console.log(user2?.profile?.address?.city); // undefined(不報錯)
// 方法呼叫:方法不存在時也不報錯
const result = user?.getName?.();
// 陣列元素存取
const first = arr?.[0];
|
Nullish Coalescing(??)#
?? 是 ES2020 引入的運算子,用來提供預設值。與 || 的差別在於觸發條件不同:
| 運算子 | 使用預設值的時機 |
|---|
|| | 左側為任何 falsy 值(0、""、false、null、undefined) |
?? | 左側僅為 null 或 undefined |
|| 的問題是 0、""、false 這些「有意義的值」也會被視為 falsy,導致預設值誤蓋掉正確的資料:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| const config = { timeout: 0, name: "", debug: false };
// || 的問題
config.timeout || 3000 // 3000(錯誤!0 是有效值,不應套用預設)
config.name || "匿名" // "匿名"(錯誤!空字串是有效值)
config.debug || true // true(錯誤!false 是有效值)
// ?? 只在 null / undefined 時才套用預設值
config.timeout ?? 3000 // 0(正確)
config.name ?? "匿名" // ""(正確)
config.debug ?? true // false(正確)
// 未設定的屬性才會套用預設
config.retries ?? 3 // 3(config.retries 是 undefined)
|
結論:需要提供預設值時,優先使用 ?? 而非 ||,避免誤判合法的 falsy 值。
JSON#
JSON(JavaScript Object Notation)是一種純文字的資料格式,常用於前後端之間的資料交換。它的語法與 JavaScript 物件很像,但有嚴格限制:
- key 必須用雙引號包住
- 值只能是:字串、數字、布林、陣列、物件、
null - 不支援函式、
undefined、Symbol - 不支援註解
物件 ↔ JSON 字串#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
| const user = { name: "Alice", age: 25, hobbies: ["閱讀", "程式"] };
// 物件 → JSON 字串(序列化)
const json = JSON.stringify(user);
console.log(json);
// {"name":"Alice","age":25,"hobbies":["閱讀","程式"]}
// 第三個參數指定縮排,產生易讀的格式
console.log(JSON.stringify(user, null, 2));
// {
// "name": "Alice",
// "age": 25,
// "hobbies": [
// "閱讀",
// "程式"
// ]
// }
// JSON 字串 → 物件(反序列化)
const parsed = JSON.parse(json);
console.log(parsed.name); // "Alice"
console.log(parsed.hobbies); // ["閱讀", "程式"]
|
注意:函式和 undefined 在 JSON.stringify 時會被直接忽略:
1
2
3
| const obj = { name: "Alice", fn: function() {}, score: undefined };
console.log(JSON.stringify(obj)); // {"name":"Alice"}
// fn 和 score 都消失了
|
淺拷貝 vs 深拷貝#
JavaScript 物件賦值是傳參考,不是複製。直接賦值給另一個變數,兩者指向同一個物件:
1
2
3
4
| const a = { x: 1 };
const b = a; // b 和 a 指向同一個物件
b.x = 99;
console.log(a.x); // 99(a 也被改到了!)
|
淺拷貝(Shallow Copy)#
建立新物件,複製第一層的屬性。但若屬性值是物件,仍然共用同一個參考:
1
2
3
4
5
6
7
8
9
| const original = { a: 1, nested: { b: 2 } };
const shallow = { ...original }; // 或 Object.assign({}, original)
shallow.a = 99; // 修改第一層:原始物件不受影響
shallow.nested.b = 99; // 修改巢狀物件:原始物件也被改到!
console.log(original.a); // 1(不受影響)
console.log(original.nested.b); // 99(被改到了!)
|
深拷貝(Deep Copy)#
完全獨立的複製,巢狀物件也一起複製,修改不互相影響:
1
2
3
4
5
6
7
8
9
10
| const original = { a: 1, nested: { b: 2 } };
// 方法一:JSON 序列化(簡單,但無法處理函式、Date、undefined)
const deep1 = JSON.parse(JSON.stringify(original));
// 方法二:structuredClone(現代瀏覽器,支援更多型別)
const deep2 = structuredClone(original);
deep2.nested.b = 999;
console.log(original.nested.b); // 2(完全不受影響)
|
| 方式 | 支援型別 | 限制 |
|---|
{ ...obj } | 所有 | 只有第一層(淺拷貝) |
JSON.parse(JSON.stringify(...)) | 基本型別、陣列、物件 | 不支援函式、Date、undefined |
structuredClone | 大多數型別含 Date | 不支援函式 |
Reference#