TypeScript 访问修饰符

156 阅读4分钟

TypeScript访问控制

在面向对象编程中,访问控制是封装的关键要素。TypeScript 提供了强大的访问修饰符系统,允许开发者精确控制类成员的可见性和可访问性。本文将深入探讨 TypeScript 的访问修饰符机制,帮助你构建更安全、更健壮的应用程序结构。

一、为什么需要访问修饰符?

访问修饰符的核心价值在于封装

  • 保护内部状态:防止外部代码直接修改对象内部状态
  • 定义清晰的API边界:明确哪些是公共接口,哪些是实现细节
  • 提升代码可维护性:减少意外耦合,简化重构过程
  • 增强安全性:限制对敏感数据的访问

考虑没有访问控制的混乱情况:

class BankAccount {
  balance: number = 0;
  
  // 任何人都可以直接修改余额
}

const account = new BankAccount();
account.balance = 1000000; // 随意修改余额!

现在,让我们看看如何使用访问修饰符解决这些问题。

二、TypeScript 的访问修饰符类型

1. public - 公共访问(默认)

  • 所有位置均可访问:类内部、外部、子类
  • 默认行为:未指定修饰符时自动为public
  • 适用场景:公共API、需要被广泛访问的方法
class User {
  // 显式声明公共属性
  public name: string;
  
  // 等同于 public age: number
  age: number;
  
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
  
  public greet() {
    console.log(`Hello, I'm ${this.name}`);
  }
}

const user = new User("Alice", 30);
console.log(user.name); // 直接访问公共属性
user.greet(); // 调用公共方法

2. private - 私有访问

  • 仅类内部可访问:其他类无法访问(包括子类)
  • 编译时保护:TypeScript编译器会阻止外部访问
  • 运行时保护:在ES2022+环境中实现真正的私有性
class BankAccount {
  private balance: number = 0;
  
  public deposit(amount: number) {
    if (amount > 0) {
      this.balance += amount; // 内部访问允许
    }
  }
  
  public withdraw(amount: number): number {
    if (amount > 0 && amount <= this.balance) {
      this.balance -= amount;
      return amount;
    }
    return 0;
  }
  
  public getBalance(): number {
    return this.balance;
  }
}

const account = new BankAccount();
account.deposit(1000);
console.log(account.getBalance()); // 1000

// account.balance = 10000; // 错误: 'balance' 是私有的
// Property 'balance' is private and only accessible within class 'BankAccount'.

3. protected - 受保护访问

  • 类内部和子类可访问:对外部代码不可见
  • 继承体系共享:允许子类访问父类受保护成员
  • 适用场景:构建可扩展的类库
class Animal {
  protected name: string;
  
  constructor(name: string) {
    this.name = name;
  }
  
  protected move(distance: number) {
    console.log(`${this.name} moved ${distance}m.`);
  }
}

class Dog extends Animal {
  private breed: string;
  
  constructor(name: string, breed: string) {
    super(name);
    this.breed = breed;
  }
  
  public bark() {
    console.log(`${this.name} the ${this.breed} barks!`);
  }
  
  public run(distance: number) {
    // 子类访问父类的protected方法
    this.move(distance);
    console.log("...at full speed!");
  }
}

const myDog = new Dog("Max", "Retriever");
myDog.bark(); // "Max the Retriever barks!"
myDog.run(10); // "Max moved 10m. ...at full speed!"

// myDog.name; // 错误: 'name' 是受保护的
// myDog.move(5); // 错误: 'move' 是受保护的

三、特殊成员修饰符

1. readonly - 只读修饰符

  • 不可变属性:初始化后不能重新赋值
  • 可与访问修饰符组合:如 public readonlyprivate readonly
  • 编译时约束:确保不可变性
class Configuration {
  public readonly apiUrl: string;
  private readonly apiKey: string;
  
  constructor(url: string, key: string) {
    this.apiUrl = url;
    this.apiKey = key;
  }
  
  public fetchData() {
    console.log(`Fetching from ${this.apiUrl} with key ${this.apiKey}`);
    // ...
  }
}

const config = new Configuration("https://api.example.com", "abc123");
config.fetchData();

// config.apiUrl = "https://new-url.com"; // 错误: 无法分配到"apiUrl",因为它是只读属性
// config.apiKey = "new-key"; // 错误: 私有且只读

2. ECMAScript私有字段(#前缀)

  • 真正的运行时私有:不仅是编译时检查
  • TypeScript 3.8+支持:遵循ECMAScript标准
  • 避免意外访问:比private更严格的访问控制
class SecureStorage {
  #encryptionKey: string;
  
  constructor(key: string) {
    this.#encryptionKey = key;
  }
  
  encrypt(data: string) {
    // 使用#encryptionKey进行加密...
    console.log(`Encrypting with key starting with ${this.#encryptionKey.substring(0, 3)}...`);
    return `${data}_encrypted`;
  }
}

const storage = new SecureStorage("secret123");
storage.encrypt("data");

// storage.#encryptionKey; // 语法错误:不能在类外访问

四、构造函数中的参数属性

TypeScript 提供了简洁的语法,允许在构造函数参数前直接添加修饰符:

class Point {
  // 直接在构造函数中声明成员
  constructor(
    public x: number, 
    public y: number,
    private readonly id: string
  ) {
    // 不需要显式赋值
  }
  
  public getPosition() {
    return `Point ${this.id} is at (${this.x}, ${this.y})`;
  }
}

const p = new Point(10, 20, "p1");
console.log(p.getPosition()); // "Point p1 is at (10, 20)"
console.log(p.x, p.y); // 10, 20
// console.log(p.id); // 错误: 'id' 是私有的

五、访问器(Getters/Setters)

通过 getter 和 setter 实现更精细的访问控制:

class Temperature {
  private _celsius: number = 0;
  
  constructor(celsius: number) {
    this.celsius = celsius; // 使用setter
  }
  
  // Getter
  get celsius(): number {
    return this._celsius;
  }
  
  // Setter
  set celsius(value: number) {
    if (value < -273.15) {
      throw new Error("Temperature below absolute zero is not possible");
    }
    this._celsius = value;
  }
  
  // 计算属性
  get fahrenheit(): number {
    return this._celsius * 1.8 + 32;
  }
  
  set fahrenheit(value: number) {
    this.celsius = (value - 32) / 1.8;
  }
}

const temp = new Temperature(25);
console.log(temp.celsius); // 25 (通过getter)
console.log(temp.fahrenheit); // 77 (通过getter计算)

temp.celsius = 30;
console.log(temp.fahrenheit); // 86

temp.fahrenheit = 100;
console.log(temp.celsius); // ≈37.78

// temp.celsius = -300; // 抛出错误: "Temperature below absolute zero..."

六、访问修饰符与继承

在继承层次结构中,访问修饰符遵循特定规则:

class Base {
  public publicProp = "public";
  protected protectedProp = "protected";
  private privateProp = "private";
}

class Derived extends Base {
  accessBaseMembers() {
    console.log(this.publicProp); // √ 允许
    console.log(this.protectedProp); // √ 允许(子类访问)
    // console.log(this.privateProp); // × 错误:私有成员
  }
}

const derived = new Derived();
console.log(derived.publicProp); // √ 允许
// console.log(derived.protectedProp); // × 错误:受保护成员
// console.log(derived.privateProp); // × 错误:私有成员

七、实际应用场景

1. UI组件开发:封装内部状态

abstract class UIComponent {
  protected element: HTMLElement;
  
  constructor(tag: string) {
    this.element = document.createElement(tag);
  }
  
  protected abstract renderContent(): void;
  
  public mount(container: HTMLElement) {
    this.renderContent();
    container.appendChild(this.element);
  }
}

class Button extends UIComponent {
  private label: string;
  
  constructor(label: string) {
    super("button");
    this.label = label;
  }
  
  protected renderContent() {
    this.element.textContent = this.label;
    this.element.addEventListener("click", this.handleClick);
  }
  
  private handleClick = () => {
    console.log(`Button "${this.label}" clicked`);
  };
}

// 使用
const button = new Button("Submit");
button.mount(document.body);

2. API客户端:保护凭证信息

class ApiClient {
  private readonly baseUrl: string;
  private readonly accessToken: string;
  
  constructor(baseUrl: string, accessToken: string) {
    this.baseUrl = baseUrl;
    this.accessToken = accessToken;
  }
  
  public async get(endpoint: string): Promise<any> {
    const url = `${this.baseUrl}/${endpoint}`;
    const response = await fetch(url, {
      headers: this.getAuthHeaders()
    });
    return response.json();
  }
  
  private getAuthHeaders() {
    return {
      Authorization: `Bearer ${this.accessToken}`
    };
  }
}

// 使用
const api = new ApiClient("https://api.example.com", "secure_token_123");
const data = await api.get("users");

3. 状态管理:受保护的状态更改

class Store<T> {
  private state: T;
  private subscribers: Array<(state: T) => void> = [];
  
  constructor(initialState: T) {
    this.state = initialState;
  }
  
  public getState(): T {
    return this.state;
  }
  
  // 保护setState方法不被外部调用
  protected setState(updater: (current: T) => T) {
    this.state = updater(this.state);
    this.notifySubscribers();
  }
  
  public subscribe(callback: (state: T) => void) {
    this.subscribers.push(callback);
    return () => {
      this.subscribers = this.subscribers.filter(sub => sub !== callback);
    };
  }
  
  private notifySubscribers() {
    this.subscribers.forEach(sub => sub(this.state));
  }
}

class CounterStore extends Store<number> {
  constructor() {
    super(0);
  }
  
  public increment() {
    this.setState(current => current + 1);
  }
  
  public decrement() {
    this.setState(current => current - 1);
  }
}

const counter = new CounterStore();
counter.subscribe(state => console.log("Count:", state));
counter.increment(); // 输出: Count: 1
counter.increment(); // 输出: Count: 2
counter.decrement(); // 输出: Count: 1

八、访问修饰符的最佳实践

  1. 最小权限原则:始终从private开始,仅在必要时放宽

    class SecureComponent {
      // 优先设为私有
      private internalData = "...";
      
      // 仅在需要时设为受保护
      protected extensionPoint = "...";
      
      // 公共API保持精简
      public publicMethod() { ... }
    }
    
  2. 避免公共可变状态:使用访问器控制修改

    // 不推荐 ❌
    class BadExample {
      public mutableData: any;
    }
    
    // 推荐 ✅
    class GoodExample {
      private _data: any;
      
      public get data() {
        return this._data;
      }
      
      protected set data(value) {
        // 验证逻辑
        this._data = value;
      }
    }
    
  3. 使用readonly实现不可变性

    class Configuration {
      // 设置后不可修改
      public readonly apiEndpoint: string;
      
      constructor(endpoint: string) {
        this.apiEndpoint = endpoint;
      }
    }
    
  4. 组合修饰符实现复杂控制

    class Advanced {
      // 公开但只读
      public readonly published: string;
      
      // 子类可访问但不可修改
      protected readonly internalConfig: object;
      
      // 仅在类内访问且不可改
      private readonly coreSecret: string;
    }
    
  5. 优先使用ECMAScript私有字段(#)

    class ModernClass {
      #realPrivate = "truly hidden";
      
      showPrivate() {
        console.log(this.#realPrivate);
      }
    }
    
    const instance = new ModernClass();
    // instance.#realPrivate; // 语法错误
    

九、与其他语言的对比

特性TypeScriptJavaPythonC#
默认访问public包私有公开private
私有修饰符private, #private__前缀(约定)private
受保护protectedprotected_前缀(约定)protected
只读readonlyfinal无内置支持readonly
访问器get/setget/set方法@propertyget/set

十、掌握访问修饰符

TypeScript 的访问修饰符系统提供了多层级的可见性控制:

  1. public:默认访问级别,适合公共API
  2. private:内部实现细节,防止外部访问
  3. protected:支持扩展,允许子类访问
  4. readonly:实现不可变属性
  5. #fields:真正的运行时私有字段

核心价值

  • 封装保护:保护内部状态不被意外修改
  • 接口清晰:区分公共接口与实现细节
  • 继承控制:设计可扩展的类层次结构
  • 不可变性:通过readonly保证状态一致性

"良好的访问控制不是限制自由,而是定义清晰边界,让系统各部分在安全环境中协同工作。" —— TypeScript设计哲学

在项目中合理应用访问修饰符,将大大提高代码的健壮性、可维护性和安全性。遵循最佳实践,从最严格的private开始,仅在必要时放宽访问级别,是构建高质量TypeScript代码的关键!