TypeScript 中 public、private、protected、readonly 和 abstract 有什么区别?
简化版
public 表示类内外都能访问,private 只能类内部访问,protected 允许类内部和子类访问;readonly 限制属性初始化后不能再赋值;abstract 用来定义不能直接实例化的抽象类和必须由子类实现的抽象成员。它们主要是 TypeScript 的类型检查约束,不等同于完整的运行时权限系统。
详细版
示例:
abstract class Service {
public readonly name: string;
protected baseUrl: string;
private token: string;
constructor(name: string, baseUrl: string, token: string) {
this.name = name;
this.baseUrl = baseUrl;
this.token = token;
}
abstract request(path: string): Promise<unknown>;
}
面试时要区分访问范围、继承范围和运行时边界。尤其要说明:TS 的 private 是类型层限制;如果需要 JavaScript 原生运行时私有字段,应使用 #field。
完整版教学
一、访问修饰符解决类成员暴露边界
类一旦用于业务建模,就会遇到哪些属性能被外部访问、哪些只能内部维护的问题。 访问修饰符就是用来表达边界的。 它让类的使用者知道哪些是稳定 API,哪些是内部实现细节。
class Counter {
public value = 0;
private step = 1;
public inc() {
this.value += this.step;
}
}
这里外部可以读写 value,但不能直接访问 step。
如果 10 个调用方都直接改 step,类内部以后就很难维护。
访问修饰符的价值不是“防黑客”,而是让代码结构更清楚。
二、public 是默认开放成员
public 表示类内部、子类、实例外部都可以访问。
TypeScript 类成员默认就是 public,所以很多时候可以省略。
但在公共 SDK、团队代码规范或教学场景里,显式写出来能让意图更明显。
class User {
public id: string;
public constructor(id: string) {
this.id = id;
}
public getId() {
return this.id;
}
}
| 修饰符 | 类内部 | 子类 | 实例外部 |
|---|---|---|---|
public | 可以 | 可以 | 可以 |
protected | 可以 | 可以 | 不可以 |
private | 可以 | 不可以 | 不可以 |
当面试官问默认访问级别时,答案是 public。
但真实项目中,不建议为了省事把所有成员都暴露为 public。
三、private 是 TypeScript 类型层私有
private 限制成员只能在当前类内部访问。
子类和外部实例都不能访问。
但传统 TS private 主要是编译期检查,不等同于 JavaScript 原生 #private 的运行时私有。
class TokenStore {
private token = 'abc';
getToken() {
return this.token;
}
}
const store = new TokenStore();
// store.token; // TS 报错
如果编译目标和运行环境支持,#token 才是 JavaScript 运行时私有字段。
class RealPrivateStore {
#token = 'abc';
}
这也是高频追问点:TS private 更像类型系统的封装约束,#private 是语言运行时语义。
四、protected 给继承留扩展点
protected 允许当前类和子类访问,但实例外部不能访问。
它适合基类给子类预留扩展能力,又不想暴露给普通调用方。
例如请求基类可以把 baseUrl 交给子类拼路径,但不让页面组件随便改。
class ApiClient {
protected baseUrl = 'https://api.example.com';
}
class UserClient extends ApiClient {
getUserUrl(id: string) {
return `${this.baseUrl}/users/${id}`;
}
}
一个数字化理解:如果 5 个子类都需要访问同一段基础配置,把它设为 protected 比复制 5 份更好;但如果外部页面也能随意改它,就可能破坏所有子类行为。
所以 protected 是“给继承体系内部用”的门。
五、readonly 限制重新赋值,不等于深度不可变
readonly 表示属性只能在声明处或构造函数中赋值。
它常用于 id、创建时间、配置引用等不应该被重新绑定的成员。
但如果属性本身是对象,readonly 默认只限制这一层引用,不保证深层对象不可变。
class Order {
readonly id: string;
readonly meta: { status: string };
constructor(id: string) {
this.id = id;
this.meta = { status: 'created' };
}
}
const order = new Order('o1');
// order.id = 'o2'; // 报错
order.meta.status = 'paid'; // 默认允许
如果要深度不可变,需要结合 Readonly<T>、递归工具类型、冻结对象或不可变数据约定。
面试回答里要避免把 readonly 说成“对象完全不能变”。
六、abstract 定义不能直接实例化的基类契约
abstract class 不能直接 new。
它可以包含已实现方法,也可以声明抽象方法,要求子类必须实现。
这适合多个实现共享部分逻辑,同时又必须提供差异化行为的场景。
abstract class StorageAdapter {
abstract get(key: string): Promise<string | null>;
async getJSON<T>(key: string): Promise<T | null> {
const raw = await this.get(key);
return raw ? JSON.parse(raw) as T : null;
}
}
class LocalStorageAdapter extends StorageAdapter {
async get(key: string) {
return localStorage.getItem(key);
}
}
流程如下:
抽象类定义公共流程
-> 抽象方法留下差异点
-> 子类实现差异点
-> 调用方依赖统一基类能力
它和 interface 的区别是:抽象类可以带实现和状态,interface 主要描述结构契约。
记忆钩子:
public给所有人,protected给家族,private给自己,readonly管赋值,abstract管模板和约束。
七、常见误区与追问
- 误区:TypeScript private 一定能阻止运行时访问。 传统
private主要是编译期约束,运行时强私有应考虑 JavaScript#field。 - 误区:readonly 表示对象深层完全不可变。
readonly默认限制属性重新赋值,不自动冻结嵌套对象。 - 误区:protected 和 private 都只是不能外部访问,所以一样。
protected子类能访问,private子类也不能访问。 - 追问:abstract class 和 interface 怎么选? 只描述形状用 interface;需要共享实现、状态或模板方法时用 abstract class。
- 追问:构造函数参数前加 public/private 是什么语法? 这是参数属性简写,会自动声明并初始化同名成员。
- 追问:为什么不把所有字段都设成 public? public 会扩大 API 面,外部依赖越多,后续重构成本越高。
八、加强记忆
这题围绕“访问边界”和“运行时边界”回答。先用表格说清 public/protected/private 的访问范围,再补 readonly 只管赋值、abstract 定义基类契约,最后强调 TS 修饰符多数是类型层约束;如果面试官追问真正私有,就拿 #field 对比。