JavaScript 中的 `Object` 類別

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
  • 通過 gettersetter,可以將屬性存取當作外部 API,並加入驗證或副作用。

原型與繼承(prototype / __proto__ / Object.getPrototypeOf

JavaScript 的繼承是基於原型鏈,而不是類別。物件可以將其他物件作為自己的原型。

Object.getPrototypeOfObject.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

hasOwnPropertyisPrototypeOftoStringvalueOf 定義了物件的基本行為。

 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.keysObject.valuesObject.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.freezeObject.sealObject.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.definePropertyObject.assignObject.freeze 這些 API 很重要,並且設計時要小心避免原型污染和淺拷貝等陷阱。

您可以在我們的 YouTube 頻道上使用 Visual Studio Code 來跟隨上述文章一起學習。 請也查看我們的 YouTube 頻道。

YouTube Video