← 返回题目列表

TypeScript 中 public、private、protected、readonly 和 abstract 有什么区别?

高频 中等 第 18 / 32 题 更新于 2026/07/29
TypeScriptclass访问修饰符readonlyabstract

简化版

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 对比。