类成员的访问修饰符

在 TypeScript 中,访问修饰符是面向对象编程的重要特性,它们控制着类成员(属性和方法)的可访问性。合理使用访问修饰符能够实现封装性,提高代码的安全性和可维护性。本文将深入探讨 TypeScript 中的四种访问修饰符:publicprivateprotectedreadonly,帮助前端开发者掌握这一重要知识点。

一、public 修饰符

1.1 基本用法

public 是默认的访问修饰符。当一个类成员被标记为 public 时,它可以在任何地方被访问:

typescript 复制代码
class Person {
  public name: string;
  
  constructor(name: string) {
    this.name = name;
  }
  
  public greet() {
    return `Hello, my name is ${this.name}`;
  }
}

const person = new Person("Alice");
console.log(person.name);      // 可以访问
console.log(person.greet());   // 可以调用

1.2 默认行为

即使不显式声明 public,类成员默认也是公共的:

typescript 复制代码
class Person {
  name: string;  // 默认为 public
  
  constructor(name: string) {
    this.name = name;
  }
}

1.3 使用场景

  • 当需要对外暴露接口时
  • 当成员需要在类外部被访问或修改时
  • 作为类的公共 API 的一部分

二、private 修饰符

2.1 基本用法

private 修饰符限制成员只能在类内部访问:

typescript 复制代码
class BankAccount {
  private balance: number;
  
  constructor(initialBalance: number) {
    this.balance = initialBalance;
  }
  
  public deposit(amount: number) {
    if (amount > 0) {
      this.balance += amount;  // 类内部可以访问
    }
  }
  
  public getBalance(): number {
    return this.balance;  // 类内部可以访问
  }
}

const account = new BankAccount(1000);
// console.log(account.balance);  // 错误:balance 是私有的
console.log(account.getBalance());  // 正确:通过公共方法访问

2.2 编译时的检查

TypeScript 的 private 是编译时的约束,在运行时仍然可以访问:

typescript 复制代码
// 编译时报错,但运行时实际上可以访问
// console.log(account['balance']);  // 编译时报错,但运行时可以

2.3 使用场景

  • 封装内部实现细节
  • 保护敏感数据不被外部直接修改
  • 防止外部代码依赖内部实现

三、protected 修饰符

3.1 基本用法

protected 修饰符允许成员在类内部和子类中访问:

typescript 复制代码
class Animal {
  protected name: string;
  
  constructor(name: string) {
    this.name = name;
  }
  
  protected move() {
    console.log(`${this.name} is moving`);
  }
}

class Dog extends Animal {
  public bark() {
    console.log(`${this.name} is barking`);  // 子类可以访问 protected 成员
    this.move();  // 子类可以调用 protected 方法
  }
}

const dog = new Dog("Buddy");
dog.bark();
// dog.name;  // 错误:外部不能访问 protected 成员
// dog.move();  // 错误:外部不能调用 protected 方法

3.2 构造函数中的 protected

构造函数也可以被标记为 protected,这意味着这个类不能被实例化,只能被继承:

typescript 复制代码
class AbstractClass {
  protected constructor() {
    // 只能被继承,不能直接实例化
  }
}

class ConcreteClass extends AbstractClass {
  constructor() {
    super();
  }
}

// const obj = new AbstractClass();  // 错误:构造函数是 protected
const obj = new ConcreteClass();  // 正确

3.3 使用场景

  • 定义抽象基类,提供可被子类重用的功能
  • 实现模板方法模式
  • 保护某些只能在继承体系中使用的成员

四、readonly 修饰符

4.1 基本用法

readonly 修饰符使属性只能在声明时或构造函数中被初始化:

typescript 复制代码
class Circle {
  readonly pi: number = 3.14159;
  readonly radius: number;
  
  constructor(radius: number) {
    this.radius = radius;  // 构造函数中可以赋值
  }
  
  calculateArea() {
    return this.pi * this.radius * this.radius;
  }
  
  // 错误:不能在方法中修改 readonly 属性
  // setRadius(newRadius: number) {
  //   this.radius = newRadius;
  // }
}

const circle = new Circle(5);
// circle.radius = 10;  // 错误:不能在外部修改

4.2 与其它修饰符的组合

readonly 可以与其它访问修饰符组合使用:

typescript 复制代码
class Example {
  public readonly publicReadonly: string = "public";
  private readonly privateReadonly: string = "private";
  protected readonly protectedReadonly: string = "protected";
}

4.3 使用场景

  • 定义常量或不可变属性
  • 确保对象的部分状态在初始化后不会被修改
  • 提高代码的可预测性和安全性

五、参数属性

TypeScript 提供了简洁的语法,可以在构造函数参数中直接声明和初始化成员:

typescript 复制代码
class User {
  constructor(
    public id: number,
    private name: string,
    protected email: string,
    readonly createdAt: Date = new Date()
  ) {
    // 无需手动赋值,TypeScript 会自动处理
  }
}

// 等价于:
class UserTraditional {
  public id: number;
  private name: string;
  protected email: string;
  readonly createdAt: Date;
  
  constructor(id: number, name: string, email: string) {
    this.id = id;
    this.name = name;
    this.email = email;
    this.createdAt = new Date();
  }
}

六、最佳实践和注意事项

6.1 封装性原则

优先使用最严格的访问级别,只在必要时放宽限制:

typescript 复制代码
// 不好的做法:过度暴露内部实现
class BadExample {
  public internalData: any;
  public internalMethod() {}
}

// 好的做法:最小化暴露
class GoodExample {
  private internalData: any;
  private internalMethod() {}
  
  public publicInterface() {
    // 通过公共方法提供受控的访问
  }
}

6.2 与 JavaScript 私有字段的区别

TypeScript 3.8+ 引入了 ECMAScript 私有字段语法,使用 # 前缀:

typescript 复制代码
class ModernExample {
  #realPrivateField: string;  // 真正的私有字段,运行时也私有
  
  constructor() {
    this.#realPrivateField = "secret";
  }
}

与 TypeScript 的 private 相比:

  • # 字段是真正的运行时私有
  • private 只是编译时检查
  • 根据需求选择合适的方式

6.3 设计考虑

  1. 面向接口编程:通过公共方法暴露功能,隐藏实现细节
  2. 可测试性:适当的封装使单元测试更容易
  3. 可维护性:减少外部代码对内部实现的依赖

七、总结

TypeScript 的访问修饰符提供了强大的封装能力:

  1. public:默认修饰符,允许在任何地方访问
  2. private:限制只能在类内部访问
  3. protected:允许在类内部和子类中访问
  4. readonly:使属性只读,只能在声明时或构造函数中初始化

合理使用这些修饰符可以:

  • 提高代码的安全性和可靠性
  • 减少意外的副作用
  • 使代码更易于理解和维护
  • 支持更好的面向对象设计

掌握 TypeScript 的访问修饰符是成为高级前端开发者的重要一步,它不仅能帮助你在面试中脱颖而出,更能在实际开发中写出更健壮、可维护的代码。