在面向对象编程中,访问控制是封装的关键要素。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 readonly或private 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
八、访问修饰符的最佳实践
-
最小权限原则:始终从
private开始,仅在必要时放宽class SecureComponent { // 优先设为私有 private internalData = "..."; // 仅在需要时设为受保护 protected extensionPoint = "..."; // 公共API保持精简 public publicMethod() { ... } } -
避免公共可变状态:使用访问器控制修改
// 不推荐 ❌ class BadExample { public mutableData: any; } // 推荐 ✅ class GoodExample { private _data: any; public get data() { return this._data; } protected set data(value) { // 验证逻辑 this._data = value; } } -
使用
readonly实现不可变性class Configuration { // 设置后不可修改 public readonly apiEndpoint: string; constructor(endpoint: string) { this.apiEndpoint = endpoint; } } -
组合修饰符实现复杂控制
class Advanced { // 公开但只读 public readonly published: string; // 子类可访问但不可修改 protected readonly internalConfig: object; // 仅在类内访问且不可改 private readonly coreSecret: string; } -
优先使用ECMAScript私有字段(
#)class ModernClass { #realPrivate = "truly hidden"; showPrivate() { console.log(this.#realPrivate); } } const instance = new ModernClass(); // instance.#realPrivate; // 语法错误
九、与其他语言的对比
| 特性 | TypeScript | Java | Python | C# |
|---|---|---|---|---|
| 默认访问 | public | 包私有 | 公开 | private |
| 私有修饰符 | private, # | private | __前缀(约定) | private |
| 受保护 | protected | protected | _前缀(约定) | protected |
| 只读 | readonly | final | 无内置支持 | readonly |
| 访问器 | get/set | get/set方法 | @property | get/set |
十、掌握访问修饰符
TypeScript 的访问修饰符系统提供了多层级的可见性控制:
public:默认访问级别,适合公共APIprivate:内部实现细节,防止外部访问protected:支持扩展,允许子类访问readonly:实现不可变属性#fields:真正的运行时私有字段
核心价值:
- 封装保护:保护内部状态不被意外修改
- 接口清晰:区分公共接口与实现细节
- 继承控制:设计可扩展的类层次结构
- 不可变性:通过
readonly保证状态一致性
"良好的访问控制不是限制自由,而是定义清晰边界,让系统各部分在安全环境中协同工作。" —— TypeScript设计哲学
在项目中合理应用访问修饰符,将大大提高代码的健壮性、可维护性和安全性。遵循最佳实践,从最严格的private开始,仅在必要时放宽访问级别,是构建高质量TypeScript代码的关键!