本文 AI 產出,尚未審核

TypeScript 類別(Classes)—— 公開(public)/ 私有(private)/ 受保護(protected)存取修飾子


簡介

在面向物件程式設計(OOP)中,類別是封裝資料與行為的核心概念。
TypeScript 在 ES6 之上加入了靜態型別與存取修飾子(access modifiers),讓開發者可以在編譯階段就捕捉到不當的成員使用,提升程式的可維護性與安全性。

publicprivateprotected 三種修飾子決定了類別成員(屬性或方法)在 外部、子類別以及同一個類別內 的可見度。掌握它們的差異,不僅能避免意外的資料洩漏,還能在大型專案中建立清晰的介面與實作分離,讓程式碼更易於閱讀與測試。

本篇文章將從概念說明、實作範例、常見陷阱到實務應用,完整闡述這三種存取修飾子的使用方式,幫助 初學者到中階開發者 在 TypeScript 中寫出更健全的類別。


核心概念

1. public(預設)

  • 說明:未加修飾子或明確標註 public 的成員,任何地方都可以直接存取。
  • 特性:最寬鬆的存取權限,常用於提供外部呼叫的 API。
class User {
  // 預設為 public
  name: string;
  // 明確寫出 public
  public age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  // 任何程式都能呼叫
  greet(): void {
    console.log(`Hello, I'm ${this.name}, ${this.age} years old.`);
  }
}

const alice = new User('Alice', 28);
alice.greet();               // Hello, I'm Alice, 28 years old.
console.log(alice.name);    // Alice
alice.age = 29;              // 可以直接修改

重點:若不想限制成員的可見度,直接使用 public(或省略)即可。


2. private(私有)

  • 說明:僅能在 宣告它的類別內部 被存取,外部或子類別皆不可直接使用。
  • 特性:用於隱藏實作細節、保護資料不被外部誤修改。
class BankAccount {
  private balance: number = 0;   // 私有屬性

  constructor(public owner: string) {}

  // 公開的存款方法,內部才能修改 balance
  deposit(amount: number): void {
    if (amount <= 0) throw new Error('Deposit amount must be positive');
    this.balance += amount;
  }

  // 公開的查詢方法,允許外部讀取但不直接存取
  getBalance(): number {
    return this.balance;
  }
}

const acct = new BankAccount('Bob');
acct.deposit(500);
console.log(acct.getBalance()); // 500
// console.log(acct.balance);    // ❌ 編譯錯誤:Property 'balance' is private

小技巧:若需要在類別外部只讀的屬性,可將屬性設為 private,再提供 public get 存取子(getter)。


3. protected(受保護)

  • 說明:在 宣告它的類別 以及 所有子類別 中皆可存取,外部仍不可直接使用。
  • 特性:適合「讓子類別延伸或覆寫」的情境,同時保護成員不被外部濫用。
class Animal {
  protected name: string;   // 受保護屬性

  constructor(name: string) {
    this.name = name;
  }

  // 受保護方法,子類別可以呼叫
  protected makeSound(sound: string): void {
    console.log(`${this.name} says: ${sound}`);
  }
}

class Dog extends Animal {
  constructor(name: string) {
    super(name);
  }

  bark(): void {
    // 直接使用父類別的受保護屬性與方法
    this.makeSound('Woof!');
  }
}

const dog = new Dog('Lucky');
dog.bark();                 // Lucky says: Woof!
// console.log(dog.name);   // ❌ 編譯錯誤:Property 'name' is protected

注意protected 讓子類別可以「看到」父類別的內部實作,但仍保持對外部的封裝。


4. 結合 readonly 與存取修飾子

readonly 可與任意修飾子搭配,讓屬性在 建構後不可再被賦值,常用於「常數」或「唯一識別碼」。

class Product {
  public readonly id: number;   // 只能在建構子內設定
  private _price: number;

  constructor(id: number, price: number) {
    this.id = id;
    this._price = price;
  }

  get price(): number {
    return this._price;
  }

  set price(value: number) {
    if (value < 0) throw new Error('Price cannot be negative');
    this._price = value;
  }
}

const p = new Product(101, 199);
console.log(p.id);          // 101
// p.id = 202;               // ❌ 編譯錯誤:Cannot assign to 'id' because it is a read-only property.
p.price = 149;              // 透過 setter 調整

程式碼範例彙總

以下提供 五個實用範例,展示如何在真實開發中靈活運用 publicprivateprotected,以及它們的組合。

範例編號 主題 重點說明
1 API 回傳模型 使用 public readonly 定義不可變的欄位,保證資料一致性。
2 Singleton(單例) private 建構子防止外部實例化,public static 取得唯一實例。
3 事件系統基礎類別 protected 讓子類別自行註冊與觸發事件。
4 資料驗證器 private 方法封裝驗證邏輯,public 方法提供外部介面。
5 繼承與多型 protected 屬性與抽象方法結合,實作多型行為。

範例 1:API 回傳模型

class UserDto {
  // 只讀且公開,外部可以直接讀取但無法改寫
  public readonly id: number;
  public readonly username: string;
  public readonly email: string;

  constructor(data: { id: number; username: string; email: string }) {
    this.id = data.id;
    this.username = data.username;
    this.email = data.email;
  }
}

// 使用情境
function fetchUser(): UserDto {
  // 假設從 API 取得資料
  const data = { id: 7, username: 'john_doe', email: 'john@example.com' };
  return new UserDto(data);
}

const user = fetchUser();
console.log(user.id);          // 7
// user.id = 8;                 // ❌ 編譯錯誤

範例 2:Singleton(單例)模式

class Logger {
  // 私有建構子阻止外部 new
  private constructor() {}

  private static _instance: Logger | null = null;

  // 公開靜態方法取得唯一實例
  public static getInstance(): Logger {
    if (!Logger._instance) {
      Logger._instance = new Logger();
    }
    return Logger._instance;
  }

  public log(message: string): void {
    console.log(`[LOG] ${new Date().toISOString()}: ${message}`);
  }
}

// 使用方式
const logger1 = Logger.getInstance();
const logger2 = Logger.getInstance();
logger1.log('First message');
console.log(logger1 === logger2); // true

範例 3:事件系統基礎類別(受保護成員)

type Listener = (...args: any[]) => void;

class EventEmitter {
  // 受保護的 listeners,子類別可直接操作
  protected listeners: Map<string, Listener[]> = new Map();

  public on(event: string, fn: Listener): void {
    const arr = this.listeners.get(event) ?? [];
    arr.push(fn);
    this.listeners.set(event, arr);
  }

  protected emit(event: string, ...args: any[]): void {
    const arr = this.listeners.get(event);
    if (arr) {
      arr.forEach((fn) => fn(...args));
    }
  }
}

// 子類別自行定義特定事件
class Button extends EventEmitter {
  click(): void {
    console.log('Button clicked');
    this.emit('click'); // 受保護的 emit 可直接呼叫
  }
}

const btn = new Button();
btn.on('click', () => console.log('Handler 1'));
btn.on('click', () => console.log('Handler 2'));
btn.click();
// Output:
// Button clicked
// Handler 1
// Handler 2

範例 4:資料驗證器(私有輔助函式)

class EmailValidator {
  // 公開 API
  public static isValid(email: string): boolean {
    return this.checkFormat(email) && this.checkDomain(email);
  }

  // 私有輔助方法,外部不可直接呼叫
  private static checkFormat(email: string): boolean {
    const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return regex.test(email);
  }

  private static checkDomain(email: string): boolean {
    const allowed = ['example.com', 'test.org'];
    const domain = email.split('@')[1];
    return allowed.includes(domain);
  }
}

// 使用
console.log(EmailValidator.isValid('alice@example.com')); // true
// EmailValidator.checkFormat('alice@example.com'); // ❌ 編譯錯誤

範例 5:繼承與多型(抽象類別 + protected)

abstract class Shape {
  // 受保護的屬性讓子類別直接使用
  protected color: string;

  constructor(color: string) {
    this.color = color;
  }

  // 抽象方法,子類別必須實作
  abstract area(): number;

  public describe(): void {
    console.log(`A ${this.color} shape with area ${this.area()}`);
  }
}

class Circle extends Shape {
  constructor(public radius: number, color: string) {
    super(color);
  }

  // 實作抽象方法
  area(): number {
    return Math.PI * this.radius ** 2;
  }
}

const c = new Circle(5, 'red');
c.describe(); // A red shape with area 78.53981633974483
// c.color = 'blue'; // ❌ 編譯錯誤:Property 'color' is protected

常見陷阱與最佳實踐

陷阱 說明 建議的解決方案
忘記 private / protected 仍會在 JavaScript 中可見 TS 只在編譯階段檢查,最終產出的 ES5/ES6 仍是公開屬性。 使用 #私有欄位(ES2022)或 WeakMap 方式加強執行時隱私;同時在程式碼審查時保持嚴格的存取規範。
子類別意外修改父類別的 protected 成員 protected 允許子類別直接寫入,若未加限制可能破壞父類別不變性。 盡量將可變的資料設為 private,提供 protected getterprotected setter,讓子類別只能透過受控方式修改。
過度使用 public 所有屬性皆公開會讓類別變成「資料袋」(data‑bag),失去封裝的意義。 先思考是否真的需要外部直接存取;若只是讀取,使用 private + public getter;若需要寫入,考慮 private + public setterprotected
在介面(interface)中使用 private / protected 介面只能描述公開結構,無法宣告存取修飾子。 若需要限制實作,改用 抽象類別(abstract class)配合 protected
混用 publicreadonly 產生誤解 public readonly 看似「可寫」但實際只能在建構子內賦值。 在文件或註解中說明「只能在建構時設定」;必要時使用 getter 取代直接屬性。

最佳實踐清單

  1. 預設使用 private:除非有明確需求,先將屬性設為私有,再決定是否暴露 getter/setter。
  2. 僅在需要被子類別存取時使用 protected,且盡量提供受控的抽象方法或保護式存取子。
  3. public 成員應盡量保持不變(使用 readonly),避免外部隨意改寫。
  4. 保持一致的命名慣例:私有屬性可使用底線前綴(_balance)或 # 私有欄位,以提升可讀性。
  5. 使用 TypeScript 的 strict 編譯選項,確保存取修飾子在編譯時被嚴格檢查。

實際應用場景

1. 前端 UI 元件庫

在開發一套可重用的 UI 元件(如 Button、Modal)時:

  • public:提供外部呼叫的屬性(如 labeldisabled)以及事件回呼(onClick)。
  • private:內部狀態(如動畫計時器、DOM 參考)不允許外部直接操作。
  • protected:若允許客製化子類別(例如 PrimaryButton 繼承自 Button),則將樣式相關的屬性(如 baseClass)設為 protected,讓子類別可以擴充而不暴露給最終使用者。

2. 後端服務的資料模型

在使用 ORM(如 TypeORM) 時:

  • public:資料庫欄位必須公開,以便 ORM 自動映射。
  • private:敏感資訊(如 passwordHash)應設為私有,並提供 public 的驗證方法。
  • protected:若有多層繼承的模型(如 BaseEntityUserAdminUser),共用的欄位(createdAtupdatedAt)可設為 protected,讓子類別自行決定是否公開。

3. 企業級業務規則引擎

業務規則往往需要 封裝擴充

  • 使用 abstract class + protected 方法 定義規則的骨架,子類別實作具體判斷邏輯。
  • private 方法用於內部計算或快取,防止外部誤用。
  • 最終的 public 執行介面只接受必要的參數,返回結果,保持 API 的穩定性。

總結

publicprivateprotected 是 TypeScript 類別最基礎卻最關鍵的存取控制機制。透過正確的使用:

  • 提升封裝性:隱藏不應被外部直接操作的實作細節。
  • 增進可維護性:讓類別的介面保持穩定,未來修改內部實作不會破壞使用者程式碼。
  • 支援繼承與多型protected 為子類別提供必要的延伸點,同時保護資料不被濫用。

在實務開發中,建議 以最嚴格的存取修飾子開始(即 private),再根據需求逐步放寬。結合 readonly、抽象類別與介面,能建立既安全又彈性的物件模型,為大型 TypeScript 專案奠定堅實基礎。

最後提醒:存取修飾子僅在編譯階段提供安全保證,若有真正的執行時隱私需求,請考慮使用 #私有欄位 或其他封裝技巧。祝你在 TypeScript 的世界裡寫出乾淨、可讀、可擴充的程式碼!