Menu

TypeScript 访问修饰符:public、private、protected

TypeScript 有三个访问修饰符:public、private 和 protected,另外还有 readonly。本文讲清每个修饰符允许什么,为什么 TypeScript 的 private 只是编译时检查而 JavaScript 的 #private 字段在运行时强制生效,以及该如何选择。

本页包含可运行的编辑器。编辑、运行,立即查看输出。

TypeScript 为类成员提供了三个访问修饰符:public(默认值)、protected 和 private。它们控制成员可以在哪里使用,编译器会报告任何来自错误位置的访问。

最后一行是编译错误,但看看强行运行时它打印出了什么:123-45-6789。这是本页最重要的一个事实,private 一节会解释原因。

每个修饰符允许什么

修饰符类内部子类中外部运行时强制
public(默认)是是是无需强制
protected是是否否
private是否否否
#name(JavaScript)是否否是
readonly可读,在构造函数中可写可读可读否

这些修饰符可以用在字段、方法、getter 和 setter、构造函数以及参数属性(constructor(private id: string))上。public 可写可不写,很多代码库都省略它。

private 是编译时检查

TypeScript 会把 private 和其他类型信息一起擦除。编译后的类里它就是一个普通属性,所以任何不经过类型检查器的东西都能看到它:纯 JavaScript 调用方、JSON.stringify、Object.keys,甚至 TypeScript 自己的方括号写法,这是有意保留的逃生出口。

对于 private 的本来用途,这没有问题:它是在告诉其他开发者(和你的编辑器)某个成员属于实现细节。但它不是安全边界,除非你自己把它去掉,否则 private 字段最终会出现在日志和 JSON 响应里。

#private 字段:运行时强制

JavaScript 有自己的私有字段,用 # 来写。引擎会强制执行它们:在类外部,obj.#field 甚至不是合法的语法,而且这个字段不会出现在 Object.keys、JSON.stringify 或 console.log 的结果里。

在类外部写 s.#token 会报错 TS18013(Property '#token' is not accessible outside class 'Session' because it has a private identifier),而且和 private 不同,在运行时也没有任何办法绕过去。运行时的规则见 JavaScript 私有字段。

private 与 #private:该用哪个

private x#x
由谁检查编译器JavaScript 引擎
Object.keys / JSON.stringify 能看到是否
方括号访问 obj["x"]允许不可能
子类可以声明自己的 x不行,会冲突可以,每个类都有自己的 #x
会被展开 { ...obj } 复制是否
方法上的写法private helper()#helper()

当数据在运行时也必须保持私有(令牌、库的使用者不能碰的内部状态),或者你希望 JSON.stringify 忽略它时,用 #private。当目标只是一个干净的公共 API、某个框架需要通过反射读取字段,或者要和代码库已有风格保持一致时,用 private。不要把两者混用:private #x 会报错 TS18010(An accessibility modifier cannot be used with a private identifier)。

protected 与子类

protected 成员在类内部以及任何继承它的类中都可以使用,但不能在外部通过实例访问。

有一条规则常常让人意外。在 Polygon 内部,你可以读取 this 或另一个 Polygon 上的 sides,但不能读取作为参数传入的普通 Shape 上的:当 other: Shape 时,other.sides 会报错 TS2446(Property 'sides' is protected and only accessible through an instance of class 'Polygon'. This is an instance of class 'Shape'.)。子类只能访问属于它自己这一支继承层级的对象上的 protected 成员。

子类可以通过重新声明把 protected 成员变成 public,但不能把 public 成员变成 protected 或 private。重新声明时要么带初始值(public override sides = 6),要么只声明类型(declare public sides: number)。光写一个 public sides: number; 会被 TS2564 和 TS2612 拒绝,因为在现代的类字段语义下,它会在 super() 返回后把继承来的值重置为 undefined。

readonly

readonly 禁止在构造完成后重新赋值。字段可以在声明处或构造函数中设置;之后的任何赋值都会报错 TS2540。

这里暴露出两个限制。readonly 是浅层的:数组本身不能被替换,但它的内容可以改变(把类型写成 readonly string[] 才能禁止 push)。而且和 private 一样,它在运行时会消失,所以被编译器拒绝的那次赋值仍然执行了。对于运行时也绝不能改变的值,请使用 Object.freeze 或只有 getter 没有 setter 的属性。readonly 那一页会讲到 Readonly<T> 和只读数组。

readonly 可以和访问修饰符组合使用:private readonly cache = new Map<string, number>() 是一种常见写法,用于既属于内部、又永远不会被重新赋值的字段。

常见错误

  • 把 private 当成安全措施。 它在运行时就没了。必须隐藏的数据请用 #field,也绝不要把带有机密信息的对象直接交给 JSON.stringify。
  • 到处写 public。 它本来就是默认值,加上去什么也不会改变。
  • 凡是“内部”的东西都用 protected。 如果没有子类需要它,用 private 可以让暴露面更小。
  • 以为 readonly 会冻结嵌套数据。 它只阻止对属性本身重新赋值。

常见问题

TypeScript 中有哪些访问修饰符?

public(默认值:任何地方都能访问)、protected(在类及其子类内部可访问)和 private(只能在类内部访问)。readonly 是一个单独的修饰符,它禁止在构造函数之后重新赋值,可以和这三者中的任意一个组合使用。

TypeScript 中 private 和 #private 有什么区别?

private 只由编译器检查;生成的 JavaScript 里它就是一个普通属性,所以 obj["secret"]、JSON.stringify 和 Object.keys 仍然能看到它。#secret 是 JavaScript 的私有字段:由运行时强制执行,类外部的代码根本无法读取它。

TypeScript 中的 private 真的是私有的吗?

只在编译时是。编译之后,这个字段就是一个普通属性,任何 JavaScript 代码都能读取。TypeScript 甚至允许用方括号(obj["field"])访问私有成员,作为一个逃生出口。如果运行时也必须保持私有,请使用 #field。

TypeScript 中 protected 和 private 有什么区别?

private 成员只在声明它的类内部可见。protected 成员在子类内部也可见。两者都不能在类层级之外通过实例访问。

TypeScript 中的 readonly 属性可以修改吗?

readonly 属性只能在声明处或构造函数中赋值,其他地方都不行(否则报 TS2540)。它是浅层的:readonly tags: string[] 不能被替换,但 tags.push() 依然可以执行。要连这个也禁止,请使用 readonly string[]。

Coddy 编程语言插图

和 Coddy 一起学编程

开始