访问修饰符
本教程共 80 篇 · 第 31 篇 · 更新于 2026-08-10 · 约 11 分钟阅读
本节目标:掌握 public、private、protected 三种访问修饰符,理解 TypeScript private 与 JavaScript # 的区别,知道什么时候该用哪个。
面向对象编程的三大支柱之一是封装——把内部实现藏起来,只暴露必要的接口。打个比方:你开车时踩油门、转方向盘就够了,不需要知道发动机怎么点火、变速箱怎么换挡。
TypeScript 提供了三种访问修饰符来实现封装。
public:默认开放
public 是默认修饰符。不加任何修饰符的成员就是 public,类内部、子类、类外部都能访问:
class Animal {
name: string; // 默认 public
constructor(name: string) {
this.name = name;
}
speak(): void { // 默认 public
console.log(`${this.name} 发出声音`);
}
}
const a = new Animal("小猫");
console.log(a.name); // ✅ 外部可以访问
a.speak(); // ✅ 外部可以调用
你不需要写 public,但有时为了代码可读性,团队可能会统一要求写上。写不写纯属风格偏好,不影响类型检查。
private:类内部专用
private 成员只能在定义它的类内部访问,外部和子类都碰不到:
class BankAccount {
private balance: number;
constructor(initialBalance: number) {
this.balance = initialBalance;
}
deposit(amount: number): void {
if (amount > 0) {
this.balance += amount; // ✅ 类内部可以访问
}
}
getBalance(): number {
return this.balance; // ✅ 类内部可以访问
}
}
const account = new BankAccount(1000);
account.deposit(500);
console.log(account.getBalance()); // 1500
// console.log(account.balance); // ❌ 外部不能访问
子类也不能访问 private 成员:
class Animal {
private dna = "ACGT";
protected getDNA(): string {
return this.dna; // ✅ 自己内部可以访问
}
}
class Dog extends Animal {
bark(): void {
// console.log(this.dna); // ❌ 子类也不能访问 private
console.log(this.getDNA()); // ✅ 但可以通过父类的 protected 方法间接访问
}
}
跨实例 private 访问
TypeScript 允许同一个类的不同实例之间互相访问 private 成员:
class Point {
private x: number;
private y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
equals(other: Point): boolean {
return this.x === other.x && this.y === other.y; // ✅ 可以访问 other 的 private 成员
}
}
这个行为跟 Java、C#、C++ 一致(Ruby 不一样,Ruby 不允许)。
private 的真相:编译期检查,运行时裸奔
这是 TypeScript 初学者最容易踩的坑:private 只在编译时生效,编译成 JavaScript 后完全不存在。
class Secret {
private key = "abc123";
}
const s = new Secret();
// TypeScript 编译时报错:
// console.log(s.key);
// 但编译后的 JS 里,key 就是一个普通属性
// 在 JS 代码里访问 s.key 完全不会报错
console.log((s as any).key); // 类型断言可以绕过
console.log(s["key"]); // 方括号访问也能绕过
CautionTypeScript 的
private是”软私有”(soft private)。如果你需要真正的运行时保护,不要依赖它。下面马上讲替代方案。
protected:子类可见
protected 是 private 的”放宽版”——类内部和子类可以访问,但外部不能:
class Person {
protected name: string;
constructor(name: string) {
this.name = name;
}
}
class Employee extends Person {
private department: string;
constructor(name: string, department: string) {
super(name);
this.department = department;
}
introduce(): string {
return `我是 ${this.name},在 ${this.department} 工作`; // ✅ 子类可以访问 protected
}
}
const emp = new Employee("Alice", "技术部");
console.log(emp.introduce()); // "我是 Alice,在 技术部 工作"
// console.log(emp.name); // ❌ 外部不能访问 protected
子类可以提升 protected 的可见性
子类重写父类的 protected 成员时,可以把可见性提升为 public:
class Base {
protected m = 10;
}
class Derived extends Base {
m = 15; // 没写修饰符,默认 public
}
const d = new Derived();
console.log(d.m); // 15 —— 没问题,m 现在是 public
如果你想保持 protected,重写时记得再加一次 protected。
跨层级的 protected 限制
TypeScript 不允许在不同分支的子类之间互相访问 protected 成员:
class Base {
protected x = 1;
}
class Derived1 extends Base {
protected x = 5;
}
class Derived2 extends Base {
f1(other: Derived2) {
other.x = 10; // ✅ 同一个类的实例,可以
}
f2(other: Derived1) {
// other.x = 10; // ❌ Derived1 不是 Derived2 的子类
}
}
TypeScript private vs JavaScript # 私有字段
前面说了 TypeScript 的 private 是软私有,运行时挡不住。如果你需要真正的硬私有,用 JavaScript 的原生私有字段 #:
class Wallet {
#balance = 0; // JS 原生私有字段
deposit(amount: number): void {
if (amount > 0) {
this.#balance += amount;
}
}
getBalance(): number {
return this.#balance;
}
}
const wallet = new Wallet();
wallet.deposit(100);
console.log(wallet.getBalance()); // 100
// 以下全部无效——运行时也访问不到
// console.log(wallet.#balance); // ❌ 编译+运行时都报错
// console.log(wallet["#balance"]); // undefined
两者怎么选?看这张表:
| 维度 | private(TS) | #(JS 原生) |
|---|---|---|
| 生效时机 | 仅编译时 | 编译时 + 运行时 |
| 单元测试中访问 | 可以用 ["prop"] 绕过 | 完全无法访问 |
| 子类继承时冲突 | 报错(不能重写) | 可以(独立作用域) |
| 编译目标兼容性 | 所有版本 | ES2022+(旧版会用 WeakMap 降级) |
| 性能 | 无运行时开销 | 极微的 WeakMap 开销(降级时) |
什么时候用哪个
用 private(TS 修饰符):
- 你只需要在编译阶段防止意外访问,不需要防恶意代码。
- 项目需要兼容旧版 JS 运行环境。
- 在单元测试中需要偶尔”偷看”内部状态。
用 #(JS 原生):
- 你写的是库或 SDK,需要防止外部代码访问内部状态。
- 数据确实敏感(密钥、令牌等),不能让任何人碰。
- 你需要子类可以声明同名的私有字段而不冲突。
Tip大多数应用代码的场景下,TypeScript 的
private已经够用了——你的同事不会故意绕过类型检查来访问你的私有属性。但如果你在写一个公开的 npm 包,就应该用#。
三种修饰符总结
| 修饰符 | 类内部 | 子类 | 类外部 |
|---|---|---|---|
| public(默认) | ✅ | ✅ | ✅ |
| protected | ✅ | ✅ | ❌ |
| private | ✅ | ❌ | ❌ |
Tip一个实用原则:默认用
private,当子类需要访问时改成protected,当外部确实需要访问时才用public。最小权限原则在类设计里同样适用。
小结
public是默认,哪里都能访问。private仅在类内部可访问,编译后消失。protected类内部和子类可访问,外部不能。- TS 的
private是软私有,运行时挡不住。需要真正保护用 JS 的#私有字段。 - 默认用
private,按需放宽到protected或public。
访问修饰符让领域有了封装,但还有另一个维度的”不可变”需求——有些属性创建之后就不该被改。下一章讲 readonly 和参数属性的简写语法。