JavaScript 中的 `Object` 類別
本文將說明 JavaScript 中的 Object 類別。
本文將說明 JavaScript 中的 Object 類別,並包含實用範例。
YouTube Video
JavaScript 中的 Object 類別
Object 是一個內建物件,作為所有 JavaScript 物件的基礎。許多語言的核心功能,如屬性管理、繼承(原型鏈)、枚舉、複製和凍結,都是通過 Object 的行為來實現的。
Object.create
有許多種建立物件的方法,應根據用途適當選擇。
物件字面值(最常用)
以下程式碼展示了最簡單且最易讀的建立物件方式。
1// Create an object using object literal
2const user = {
3 name: "Alice",
4 age: 30,
5 greet() {
6 return `Hello, I'm ${this.name}`;
7 }
8};
9
10console.log(user.greet()); // "Hello, I'm Alice"
- 在此範例中,屬性和方法使用字面值來定義。它很簡單,並且通常具有較優秀的性能。
new Object() 建構函式
Object 建構函式很少被使用,但瞭解其行為仍然有幫助。
1// Create an object using the Object constructor
2const objFromCtor = new Object();
3objFromCtor.x = 10;
4objFromCtor.y = 20;
5
6console.log(objFromCtor); // { x: 10, y: 20 }
new Object()會回傳一個空物件,但字面值{}更簡短且常用。
使用 Object.create 指定原型
Object.create 用來建立具有指定原型的物件。
1// Create an object with a specified prototype
2const proto = { hello() { return "hi"; } };
3const obj = Object.create(proto);
4obj.name = "Bob";
5
6console.log(obj.hello()); // "hi"
7console.log(Object.getPrototypeOf(obj) === proto); // true
Object.create適合基於繼承的物件設計,讓你可以精確控制原型鏈。
屬性屬性(attribute)和描述子(descriptor)
屬性擁有 'value'、'writable'、'enumerable' 和 'configurable' 等屬性,這些可以通過 Object.defineProperty 詳細控制。
defineProperty 的基本範例
接下來是使用 defineProperty 定義不可列舉和唯讀屬性的範例。
1// Define a non-enumerable read-only property
2const person = { name: "Carol" };
3
4Object.defineProperty(person, "id", {
5 value: 12345,
6 writable: false,
7 enumerable: false,
8 configurable: false
9});
10
11console.log(person.id); // 12345
12console.log(Object.keys(person)); // ["name"] — "id" is non-enumerable
13person.id = 999; // silently fails or throws in strict mode
14console.log(person.id); // still 12345
- 使用
defineProperty可精確控制屬性的行為,例如是否可列舉、可否重寫及刪除。
存取器屬性(getter / setter)
利用存取器,你可以在讀取與寫入屬性時注入邏輯。
1// Use getter and setter to manage internal state
2const data = {
3 _value: 1,
4 get value() {
5 return this._value;
6 },
7 set value(v) {
8 if (typeof v === "number" && v > 0) {
9 this._value = v;
10 } else {
11 throw new Error("value must be a positive number");
12 }
13 }
14};
15
16console.log(data.value); // 1
17data.value = 5;
18console.log(data.value); // 5
19// data.value = -1; // would throw
- 通過
getter與setter,可以將屬性存取當作外部 API,並加入驗證或副作用。
原型與繼承(prototype / __proto__ / Object.getPrototypeOf)
JavaScript 的繼承是基於原型鏈,而不是類別。物件可以將其他物件作為自己的原型。
Object.getPrototypeOf 與 Object.setPrototypeOf
以下範例顯示如何檢查與設定原型。
1// Inspect and change prototype
2const base = { speak() { return "base"; } };
3const derived = Object.create(base);
4console.log(Object.getPrototypeOf(derived) === base); // true
5
6const other = { speak() { return "other"; } };
7Object.setPrototypeOf(derived, other);
8console.log(derived.speak()); // "other"
- 使用
Object.getPrototypeOf可取得物件的原型。 Object.setPrototypeOf可更改現有物件的原型,但須小心使用以免影響效能。
重要的內建方法
我們將清楚說明從 Object.prototype 提供的實例方法中挑選出的最常用和重要的方法,以及 Object 擁有的靜態方法。
hasOwnProperty, isPrototypeOf, toString, valueOf
hasOwnProperty、isPrototypeOf、toString 和 valueOf 定義了物件的基本行為。
1// Demonstrate prototype methods
2const base = { greet() { return "hello"; } };
3const child = Object.create(base);
4const a = { x: 1 };
5
6console.log(a.hasOwnProperty("x")); // true
7console.log(a.hasOwnProperty("toString")); // false — toString is inherited
8
9console.log(a.toString()); // "[object Object]" by default
10
11console.log(base.isPrototypeOf(child)); // true
12console.log(Object.prototype.isPrototypeOf(child)); // true
hasOwnProperty是檢查屬性是否屬於物件本身的重要方法。isPrototypeOf可檢查目標物件是否以自身作為原型。
Object.keys, Object.values, Object.entries
Object.keys、Object.values、Object.entries 會回傳物件自身可列舉屬性的清單。這些方法有助於迭代與轉換。
1// Keys, values and entries
2const item = { id: 1, name: "Widget", price: 9.99 };
3
4// ["id", "name", "price"]
5console.log(Object.keys(item));
6
7// [1, "Widget", 9.99]
8console.log(Object.values(item));
9
10// [["id",1], ["name","Widget"], ["price",9.99]]
11console.log(Object.entries(item));- 這些方法經常用於物件的遍歷與轉換。
Object.assign
Object.assign 用於淺拷貝和合併。請注意,原型和存取器屬性不會被複製。
1// Shallow copy / merge using Object.assign
2const target = { a: 1 };
3const source = { b: 2 };
4const result = Object.assign(target, source);
5
6console.log(result); // { a: 1, b: 2 }
7console.log(target === result); // true (merged into target)
- 對於巢狀物件,只有參考會被複製,因此深拷貝需採用其他實作。
Object.freeze, Object.seal, Object.preventExtensions
Object.freeze、Object.seal 和 Object.preventExtensions 控制物件的可變性。
1// Freeze vs seal vs preventExtensions
2const obj = { a: 1 };
3Object.freeze(obj);
4obj.a = 2; // fails silently or throws in strict mode
5delete obj.a; // fails
6
7const obj2 = { b: 2 };
8Object.seal(obj2);
9obj2.b = 3; // allowed
10// delete obj2.b; // fails
11
12const obj3 = { c: 3 };
13Object.preventExtensions(obj3);
14obj3.d = 4; // fails
freeze最為嚴格:它禁止對物件屬性做任何更動。seal可防止新增或刪除屬性,但允許更改現有屬性的值。preventExtensions只防止新增屬性;現有屬性仍可修改或刪除。
物件的可列舉性、順序,與 for...in / for...of
for...in 會列舉可列舉的屬性名稱,包含原型鏈的屬性,因此常與 hasOwnProperty 一起使用。結合 Object.keys() 與 for...of 使用更安全,也能讓你的意圖更加明確。
1// Safe enumeration
2const obj = Object.create({ inherited: true });
3obj.own = 1;
4
5for (const key in obj) {
6 if (obj.hasOwnProperty(key)) {
7 console.log("own prop:", key);
8 } else {
9 console.log("inherited prop:", key);
10 }
11}
12
13for (const key of Object.keys(obj)) {
14 console.log("key via Object.keys:", key);
15}- 屬性列舉的規則由 ECMAScript 規範訂定,有時候列舉順序是有保證的,有時則沒有。一般情況下,作為數字解析的鍵會按升序排列,其他的鍵則按照插入順序排列。
複製(拷貝)與深層拷貝
物件複製有兩種類型:使用 Object.assign 或展開語法(spread syntax)進行的淺複製,以及使用遞迴方法進行的深複製。根據情況適當選擇這些方法非常重要。
淺拷貝(展開語法 / Object.assign)
1// Shallow copy with spread operator
2const original = { a: 1, nested: { x: 10 } };
3const shallow = { ...original };
4shallow.nested.x = 99;
5console.log(original.nested.x); // 99 — nested object is shared
- 使用淺拷貝時,巢狀物件會共用參考,故原物件的更動會影響副本。
簡單深拷貝(有注意事項)
使用 JSON 方式是快速深層拷貝的方法,但會遺失函式、Date、循環參考與 undefined 的值等。要真正進行深層拷貝,需使用專門的函式庫。
1// Deep clone using JSON methods — limited use-cases only
2const source = { a: 1, d: new Date(), nested: { x: 2 } };
3const cloned = JSON.parse(JSON.stringify(source));
4console.log(cloned); // ok for plain data, but Date becomes string, functions lost
- 基於 JSON 的方法在快速處理簡單數據時很方便,但在常見情況下,其行為可能出現問題。
Mixin 與物件組合
取代多重繼承,常常使用混合(Mixin)來組合行為。
1// Simple mixin function
2const canEat = {
3 eat() { return `${this.name} eats`; }
4};
5const canWalk = {
6 walk() { return `${this.name} walks`; }
7};
8
9function createPerson(name) {
10 const person = { name };
11 return Object.assign(person, canEat, canWalk);
12}
13
14const p = createPerson("Dana");
15console.log(p.eat()); // "Dana eats"
16console.log(p.walk()); // "Dana walks"
- Mixin 很有彈性,但要注意屬性名稱衝突及可測性。
常見陷阱與最佳實踐
以下列舉一些常見陷阱與最佳做法。
-
可變性 物件預設是可變的。在有狀態管理的應用中,請考慮使用
Object.freeze或不可變資料結構庫來實現不可變資料結構。 -
原型污染 直接使用
Object.assign或迴圈將外部資料合併至物件時,對於__proto__或constructor等特殊屬性,可能造成出乎意料的副作用甚至資安風險。在直接合併前要先過濾用戶輸入。 -
for...in的陷阱for...in也會列舉原型鏈上的屬性,所以務必搭配hasOwnProperty。使用Object.keys更為明確。 -
誤用淺拷貝 請考慮是否需要深拷貝,以避免變動巢狀物件影響原物件。
實用範例:不可變物件更新模式
React 或類似函式庫常用返回新物件、而非直接變更狀態的模式。
1// Immutable update example
2const state = { todos: [{ id: 1, text: "Buy milk", done: false }] };
3
4// Toggle todo done immutably
5function toggleTodo(state, todoId) {
6 return {
7 ...state,
8 todos: state.todos.map(t => t.id === todoId ? { ...t, done: !t.done } : t)
9 };
10}
11
12const newState = toggleTodo(state, 1);
13console.log(state.todos[0].done); // false
14console.log(newState.todos[0].done); // true
- 下列程式碼是建立新狀態物件而不直接修改原有
state物件的範例。toggleTodo函式會複製todos陣列,並僅修改目標項目後傳回新物件,因此原本的state不會改變。 - 不可變更新可減少副作用並簡化狀態管理。
實用範例:安全合併(避免原型污染)
合併外部 JSON 時,應忽略 __proto__ 以防止原型污染。
1// Safe merge ignoring __proto__ keys
2function safeMerge(target, source) {
3 for (const key of Object.keys(source)) {
4 if (key === "__proto__" || key === "constructor") continue;
5 target[key] = source[key];
6 }
7 return target;
8}
9
10const target = {};
11const source = JSON.parse('{"a":1,"__proto__":{"polluted":true}}');
12safeMerge(target, source);
13console.log(target.polluted); // undefined — safe
14console.log({}.polluted); // undefined — prototype not polluted
- 這樣的防護措施在函式庫與框架中也很重要。
效能注意事項
關於效能,請留意下列幾點:。
- 避免頻繁更改原型(
Object.setPrototypeOf)或動態增刪屬性,這會妨礙引擎最佳化。 - 當產生大量小型物件時,如果使用結構統一(相同屬性集合)的物件,優化將變得更有效。
- 深拷貝成本高。盡量減少深拷貝,或考慮使用差異更新。
總結
Object 在 JavaScript 中居於核心地位,提供了物件建立、屬性控制、繼承、拷貝、可變性管理等多種功能。理解像 Object.defineProperty、Object.assign 和 Object.freeze 這些 API 很重要,並且設計時要小心避免原型污染和淺拷貝等陷阱。
您可以在我們的 YouTube 頻道上使用 Visual Studio Code 來跟隨上述文章一起學習。 請也查看我們的 YouTube 頻道。