本文 AI 產出,尚未審核

JavaScript – 原型與繼承(Prototype & Inheritance)

主題:new 運算子原理


簡介

在 JavaScript 中,new 是建立物件最常用的語法之一。它不只是「呼叫」一個函式那麼簡單,而是會觸發一連串隱藏的步驟,從 原型鏈 的設定到 建構子 的返回值,都與語言的繼承機制緊密相連。
了解 new 的內部運作,能讓你:

  • 正確地使用 建構子函式(constructor)與 類別(class)
  • 避免因為忘記 new 或濫用 new 而產生的錯誤(如 TypeError: undefined is not a function
  • 在需要手動模擬 new 時,寫出等價的程式碼,提升對原型繼承的直觀感受

本篇文章將從概念層面剖析 new 的六個步驟,搭配實用範例說明每一步的作用,並提供常見陷阱、最佳實踐與實務應用情境,讓初學者與中階開發者都能在日常開發中更自信地使用它。


核心概念

1. new 的六個隱藏步驟

當我們執行 new Constructor(arg1, arg2) 時,JavaScript 會依序完成以下動作:

步驟 說明 影響
① 建立一個空物件 Object.create(Constructor.prototype) 產生,稱為 實例物件(instance)。 此物件的 [[Prototype]](即 __proto__)指向 Constructor.prototype
② 讓實例物件執行建構子函式 呼叫 Constructorthis 會被綁定到剛建立的空物件上。 建構子內的屬性(如 this.name = ...)會被寫入實例。
③ 取得建構子返回值 若建構子 明確 回傳物件(非 nullundefined),則以該回傳值作為 new 的結果;否則回傳步驟①產生的實例物件。 可用於「工廠函式」的寫法,或在子類別建構子中返回父類別的實例。
④ 設定 [[Prototype]] 已在步驟①完成,確保實例可以繼承 prototype 上的方法與屬性。 形成原型鏈,使 instance.method() 能正確找到方法。
⑤ 產生屬性描述(Property Descriptor) 依照建構子內的屬性賦值,為每個屬性建立 [[Value]][[Writable]][[Enumerable]][[Configurable]] 等描述。 控制屬性的可寫、可列舉與可設定性。
⑥ 返回結果 最終得到的值(步驟③的回傳或步驟①的實例)交給程式繼續執行。 這就是我們在程式碼中看到的 obj

小結new 的核心是「先產生一個連結到 prototype 的空物件,再把建構子執行於該物件上」。只要掌握這個流程,就能理解為什麼 instanceofObject.getPrototypeOf 等檢測方式會如此運作。


2. 用原生 new 與手寫模擬比較

下面示範使用原生 new 建立物件,接著用自訂函式 myNew 完全模擬同樣的行為,讓讀者看到每一步的實作細節。

// ---------- 原生 new ----------
function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.greet = function () {
  console.log(`嗨,我叫 ${this.name},今年 ${this.age} 歲。`);
};

const alice = new Person('Alice', 28);
alice.greet(); // 嗨,我叫 Alice,今年 28 歲。

// ---------- 手寫 myNew ----------
function myNew(Constructor, ...args) {
  // ① 建立空物件,並把 prototype 設為 [[Prototype]]
  const obj = Object.create(Constructor.prototype);
  // ② 呼叫建構子,this 綁定到 obj
  const result = Constructor.apply(obj, args);
  // ③ 若建構子回傳物件則直接返回,否則返回 obj
  return (typeof result === 'object' && result !== null) || typeof result === 'function'
    ? result
    : obj;
}

const bob = myNew(Person, 'Bob', 35);
bob.greet(); // 嗨,我叫 Bob,今年 35 歲。

重點

  • Object.create(Constructor.prototype) 完成了 步驟①步驟④
  • Constructor.apply(obj, args) 完成了 步驟②
  • return 判斷實作了 步驟③ 的回傳規則。

3. new 與 ES6 class 的關係

ES6 引入了 class 語法,讓原型繼承看起來更像傳統 OOP 語言。然而,class 仍然是 語法糖,背後仍然依賴 new 與原型鏈。

class Animal {
  constructor(species) {
    this.species = species;
  }
  speak() {
    console.log(`${this.species} 發出聲音`);
  }
}

// 這裡的 new 仍然走過前面說的六個步驟
const cat = new Animal('貓');
cat.speak(); // 貓 發出聲音

提醒:即使使用 class,若忘記 new 會拋出 TypeError: Class constructor Animal cannot be invoked without 'new',因為 class constructors are not callable as regular functions


4. newObject.create 的差異

特性 new Constructor() Object.create(proto)
是否執行建構子 會(步驟②) 不會
原型設定 Constructor.prototype 為原型 直接以提供的 proto 為原型
返回值 可能被建構子覆寫 永遠返回新物件
適用情境 建立具備初始化邏輯的實例 想要「純粹」繼承而不執行建構子時(如深拷貝、混入)

程式碼範例

以下提供 5 個實用範例,涵蓋常見情境與細節說明。

範例 1:建構子裡返回自訂物件

function Wrapper(value) {
  this.value = value;
  // 明確回傳一個不同的物件
  return { wrapped: value };
}

const w = new Wrapper(10);
console.log(w);          // { wrapped: 10 }
console.log(w instanceof Wrapper); // false,因為回傳值取代了實例

說明:若建構子回傳 非 null/undefined 的物件,new 會直接使用該物件作為結果,原型鏈會失效。這在 工廠模式 中很常見。


範例 2:避免忘記 new 的防呆寫法

function Car(make, model) {
  // 防止忘記 new
  if (!(this instanceof Car)) {
    return new Car(make, model);
  }
  this.make = make;
  this.model = model;
}
Car.prototype.info = function () {
  console.log(`汽車:${this.make} ${this.model}`);
};

const c1 = Car('Toyota', 'Corolla'); // 沒寫 new
c1.info(); // 汽車:Toyota Corolla

說明:在建構子最前面檢查 this instanceof Car,若不是實例就自動呼叫 new,讓 API 更容錯。


範例 3:子類別繼承父類別(ES5 寫法)

// 父類別
function Person(name) {
  this.name = name;
}
Person.prototype.sayHello = function () {
  console.log(`哈囉,我是 ${this.name}`);
};

// 子類別
function Student(name, school) {
  // 呼叫父類別建構子,讓 this 繼承屬性
  Person.call(this, name);
  this.school = school;
}

// 設定原型鏈(步驟④)
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;

// 加上子類別自己的方法
Student.prototype.study = function () {
  console.log(`${this.name} 正在 ${this.school} 學習`);
};

const s = new Student('小明', '台大');
s.sayHello(); // 哈囉,我是 小明
s.study();    // 小明 正在 台大 學習

說明

  1. Person.call(this, name) 完成 步驟②(父建構子執行)。
  2. Object.create(Person.prototype) 完成 步驟④(子類別的原型指向父類別 prototype)。

範例 4:使用 newSymbol.species 控制子類別的返回值

class MyArray extends Array {
  // 讓子類別的所有衍生方法(如 map)返回原生 Array,而非 MyArray
  static get [Symbol.species]() {
    return Array;
  }
}

const arr = new MyArray(1, 2, 3);
const mapped = arr.map(x => x * 2);

console.log(mapped instanceof MyArray); // false
console.log(mapped instanceof Array);   // true

說明:即使 new 建立的是 MyArray,透過 Symbol.species 可以在內建方法返回時「改寫」new 的行為,這在 自訂容器類別 時相當有用。


範例 5:手寫 new 的完整實作(含 Object.defineProperty

function myNewFull(Constructor, ...args) {
  // ① 建立空物件,設定原型
  const instance = Object.create(Constructor.prototype);
  // ② 呼叫建構子,綁定 this
  const result = Constructor.apply(instance, args);
  // ③ 處理返回值
  const finalObj = (typeof result === 'object' && result !== null) || typeof result === 'function'
    ? result
    : instance;
  // ④ 為實例加上不可列舉的 __proto__(模擬引擎內部行為)
  Object.defineProperty(finalObj, '__proto__', {
    value: Constructor.prototype,
    writable: false,
    enumerable: false,
    configurable: false
  });
  return finalObj;
}

// 測試
function Point(x, y) {
  this.x = x;
  this.y = y;
}
Point.prototype.toString = function () {
  return `(${this.x}, ${this.y})`;
};

const p = myNewFull(Point, 5, 7);
console.log(p.toString()); // (5, 7)

說明:此實作加入了 步驟⑤(屬性描述)與 步驟④ 的更細緻控制,展示 new 背後的完整機制。


常見陷阱與最佳實踐

常見陷阱 原因 解決方式
忘記 new 建構子被當成普通函式呼叫,thisundefined(嚴格模式)或 window(非嚴格) 使用 防呆寫法(範例 2)或 ES6 class(編譯器會強制)
在建構子內返回基本型別 基本型別會被忽略,仍返回實例物件 僅在需要時返回 物件,或避免 return
原型鏈斷裂 手動改寫 prototype 後忘記設定 constructor Child.prototype = Object.create(Parent.prototype); Child.prototype.constructor = Child;
多層繼承時 instanceof 判斷錯誤 只檢查第一層原型,忘記 Symbol.hasInstance 如有特殊需求,可自行實作 Symbol.hasInstance
使用 new 呼叫箭頭函式 箭頭函式沒有自己的 [[Construct]] 內部方法 不要把箭頭函式當作建構子,改用普通函式或 class

最佳實踐

  1. 永遠使用 newclass:保持語意清晰,讓其他開發者一眼就能看出意圖。
  2. 在建構子內避免副作用:盡量只做屬性初始化,將複雜邏輯搬到原型方法或工廠函式。
  3. 使用 Object.create 進行純粹繼承:當你只想共享方法而不想執行父建構子時,Object.create 更安全。
  4. 保持 prototype 不變:在執行期間不要隨意改變 Constructor.prototype,會導致已建立實例的原型失效。
  5. 利用 Symbol.species 控制子類別返回:自訂容器或集合類別時,避免不必要的子類別實例化,提高效能。

實際應用場景

場景 為何需要 new 範例
資料模型(Model) 每筆資料需擁有獨立狀態與方法(如 savevalidate const user = new User({name:'小華'});
自訂錯誤類別 需要保留 stackmessage,同時擁有自訂屬性 class ValidationError extends Error { constructor(msg){ super(msg); this.code = 400; } }
UI 元件庫 每個元件是可獨立掛載、擁有生命周期的實例 const btn = new Button({label:'送出'});
遊戲實體(Entity) 角色、怪物等需要共享行為(move、attack)且各自持有狀態 const hero = new Character('勇者', 100);
測試框架的 Mock 物件 需要快速產生多個相同結構的假物件 function MockUser(){ this.id = generateId(); } const m1 = new MockUser();

總結

new 並非只是一個簡單的關鍵字,它是 原型與繼承 的核心機制之一。透過六個隱藏步驟,new 完成了:

  1. 物件創建(連結到建構子 prototype
  2. 建構子執行(初始化屬性)
  3. 返回值決策(支援工廠模式)
  4. 原型鏈設定(支撐繼承)

掌握這些概念後,你可以:

  • 正確地設計與使用建構子與類別
  • 在需要時手寫 new 的等價實作,深入了解底層行為
  • 針對常見錯誤設置防呆機制,提升程式碼的健壯性

最後,記得 保持語意清晰遵循最佳實踐,讓 new 成為你在 JavaScript 開發中可靠且強大的工具。祝你寫程式愉快,持續探索更深層的原型世界!