在 TypeScript 和 JavaScript 中,类成员变量(属性)的修饰符(Modifiers) 用于控制其可见性、可访问性和可变性 。两者在能力上有显著差异:TypeScript 提供了更丰富的编译时修饰符 ,而 JavaScript(ES2022 起)引入了运行时私有字段。
下面从 TypeScript 和 JavaScript 两个角度分别说明,并对比异同。
一、TypeScript 类成员变量修饰符(编译时)
TypeScript 在 编译阶段 提供以下关键字作为访问修饰符:
| 修饰符 | 含义 | 是否生成 JS 代码 | 可见性 |
|---|---|---|---|
public |
公共(默认) | ❌ 不生成额外代码 | 类内、子类、外部均可访问 |
private |
私有 | ❌ 仅类型检查,不阻止运行时访问 | 仅类内部可访问(TS 编译时报错) |
protected |
受保护 | ❌ 仅类型检查 | 类内部 + 子类可访问 |
readonly |
只读 | ❌ 仅类型检查 | 初始化后不可修改(TS 报错) |
✅ 示例(TypeScript):
typescript
class User {
public name: string; // 默认就是 public
private id: number; // TS 禁止外部访问
protected email: string; // 子类可访问
readonly createdAt: Date; // 初始化后不可改
constructor(name: string, id: number, email: string) {
this.name = name;
this.id = id;
this.email = email;
this.createdAt = new Date();
}
}
⚠️ 注意:
private/protected只在 TypeScript 编译时生效 ,编译成 JS 后,这些字段仍是普通属性,运行时仍可被访问或修改!
ini
// 编译后的 JS(无 private 保护!)
const user = new User("Alice", 1, "a@example.com");
console.log(user.id); // ✅ 能访问!JS 不报错
user.id = 999; // ✅ 能修改!
二、JavaScript 类成员变量修饰符(运行时,ES2022+)
从 ECMAScript 2022(ES13) 开始,JavaScript 原生支持 真正的私有字段(Private Fields) ,使用 # 前缀。
| 语法 | 含义 | 运行时是否私有 | 是否可被外部访问 |
|---|---|---|---|
#fieldName |
私有字段 | ✅ 是 | ❌ 完全无法从类外访问 |
| 普通字段(无前缀) | 公共字段 | ❌ 否 | ✅ 可自由访问 |
✅ 示例(JavaScript / TypeScript 均支持):
arduino
class User {
#id; // 私有字段(JS 原生私有)
name; // 公共字段
constructor(name, id) {
this.name = name;
this.#id = id; // 只能在类内部访问
}
getId() {
return this.#id; // ✅ OK
}
}
const user = new User("Bob", 2);
console.log(user.name); // ✅ "Bob"
console.log(user.#id); // ❌ SyntaxError! 无法访问
✅ 关键优势 :
#id是真正的私有,即使在运行时也无法绕过(除非用 Proxy 等 hack,但正常代码做不到)。
三、TypeScript 对 JS 私有字段的支持
TypeScript 完全支持 # 私有字段,并提供类型检查:
typescript
class User {
#id: number;
name: string;
constructor(name: string, id: number) {
this.name = name;
this.#id = id;
}
getId(): number {
return this.#id; // ✅ TS 知道这是 number
}
}
🔸 此时你不需要 用
private,因为#id已经是运行时私有。
四、对比总结
| 特性 | TypeScript private |
JavaScript #field |
|---|---|---|
| 作用时机 | 编译时(类型检查) | 运行时(真实私有) |
| 能否被外部访问 | ✅ 能(JS 无保护) | ❌ 不能 |
| 是否生成额外代码 | ❌ 否 | ✅ 是(保留 # 语法) |
| 兼容性 | 所有 JS 环境(因被擦除) | 需要 ES2022+ 或 Babel 转译 |
| 推荐场景 | 快速开发、内部项目 | 需要真正封装、库开发 |
五、最佳实践建议
✅ 优先使用 JavaScript 原生私有字段 #
- 如果目标环境支持(现代浏览器 / Node.js 12+),优先用
#fieldName。 - 它提供真正的封装,避免"假装私有"的陷阱。
✅ 在 TypeScript 中:
- 若需兼容旧环境 → 用
private(但要清楚它只是"纸面私有")。 - 若用现代环境 → 直接用
#,无需private。
✅ 不要混用:
arduino
// ❌ 不推荐:语义重复且混乱
private #id; // 错误!不能同时用
✅ readonly 仍是 TS 特有(JS 无等价物)
-
可配合
#使用:typescriptclass Config { readonly #apiUrl: string; constructor(url: string) { this.#apiUrl = url; // 初始化后不可变(TS 检查) } }
六、补充:其他相关修饰符
| 修饰符 | 语言 | 说明 |
|---|---|---|
static |
TS & JS | 静态成员(属于类,不属于实例) |
abstract |
TS only | 抽象类/方法(不能实例化) |
declare |
TS only | 声明属性存在(用于 .d.ts 或装饰器) |
✅ 总结
| 需求 | 推荐方案 |
|---|---|
| 真正的私有字段 | 使用 JavaScript #fieldName(ES2022+) |
| 仅开发时提醒(兼容旧环境) | 使用 TypeScript private |
| 只读属性 | TypeScript readonly(JS 无原生支持) |
| 公共字段 | 直接声明(TS/JS 均默认 public) |
🎯 现代项目建议 :
用#实现私有,用readonly实现只读,放弃private(除非必须兼容旧 JS) 。
这样既能获得类型安全,又能保证运行时封装性。