下划线的问题
很多年来,JavaScript 都没有私有字段。惯用做法是给属性名加一个下划线前缀,然后指望大家都遵守:
_count 看起来是私有的,其实不是。任何调用者都可以读取、修改或删除它。下划线只是门上一块礼貌的告示牌,门本身却敞开着。
现代 JavaScript 用真正的私有字段解决了这个问题,用 # 标记。
# 让字段变成私有
在声明处和每个访问处,都给字段名加上 # 前缀:
在类内部,this.#count 正常工作。在类外部,它根本不存在:
# 实际上是字段名的一部分。它不是其他语言中 private 那样的修饰关键字,而是一个记号,解析器用它去查找对象上一个独立的、受保护的槽位。所以这个错误在解析时就会出现,代码甚至还没开始运行。
私有方法和 getter
能标记为私有的不只是字段。方法、getter 和 setter 都可以加 # 前缀:
#assertPositive 是一个内部辅助方法。它不属于公开 API,把它设为真正私有,就没人会从外部误调用它,也没人能依赖它,这样你以后可以自由地重命名或删除它。
私有静态成员
静态成员也可以是私有的,同样加上 # 前缀:
私有静态成员存在于类本身上,而不是实例上。很适合用来存放不应泄露到类外部的计数器、缓存或配置。
子类看不到它们
从 Java 或 C# 转过来的人常在这里栽跟头。JavaScript 的私有字段是“类私有”,而不是“实例私有”。子类无法访问父类的私有字段:
JavaScript 没有 protected。如果子类需要这些数据,父类必须提供一个方法、getter,或者(较少见的)一个非私有字段。这是有意的设计:私有就是真的私有,继承也不会在上面开洞。
用 in 检查私有字段
有时你想确认一个对象确实属于你的类,也就是做一次“品牌检查”。在类内部,in 运算符可以和私有字段名一起使用:
因为只有 Wallet 能创建带有 #balance 的对象,所以 #balance in obj 能可靠地判断 obj 是不是一个真正的 Wallet 实例。在某些边界情况下,它比 instanceof 更快也更安全,因为私有字段无法从外部伪造。
常见陷阱:普通对象没有私有字段
私有字段存在于由类构造函数创建的实例上。如果你在一个不是用 new 创建的对象上使用它,就会抛出错误:
用一个不是 Point 的 this 调用这个方法,会在运行时抛出错误。上面的品牌检查正是基于这个机制:私有字段绑定在声明它的那个类上,而不是任何碰巧长得像的对象上。
什么时候用 #
只要某个状态或辅助方法不属于类的公开 API,就默认用私有字段。理由如下:
- 重构更自由。 调用者无法依赖他们根本看不到的内部实现。
- 真正的封装。 外部代码不会意外读取、修改或删除内部数据。
- 自动补全更干净。 编辑器不会向外部调用者提示私有成员。
确实属于接口的东西,就用公开属性。想对私有字段提供只读访问时,用 getter(get name())。别再用下划线约定了,它只是为了弥补语言的空白,而这个空白现在已经补上了。
#celsius 是隐藏的存储;celsius 和 fahrenheit 是只读视图。调用者无法破坏内部状态,类以后也可以自由地改变存储方式。
下一篇:原型
类在很大程度上是 JavaScript 原型系统的语法糖,原型才是这门语言真正建立在其上的、更早也更基础的模型。理解原型,就能明白 this 为什么会那样表现、继承究竟如何工作,以及类的 extends 在底层做了什么。这就是下一页的内容。
常见问题
JavaScript 中怎样声明私有字段?
在字段名前加上 #,声明时和每次访问时都要加。class Counter { #count = 0; increment() { this.#count++; } } 会创建一个真正私有的字段。# 是名字的一部分,不是运算符。
JavaScript 中 #field 和 _field 有什么区别?
_field 只是一种命名约定,这个属性仍然是公开的,任何人都可以读写。#field 则由语言本身强制保证:类外部的代码根本无法访问它,尝试访问会在解析时抛出 SyntaxError。想要真正的私有,就用 #。
子类能访问父类的私有字段吗?
不能。私有字段的作用域仅限于声明它的类,连子类也碰不到。如果子类需要访问,父类必须提供一个方法或 getter。这比其他语言中的 protected 更严格,而且是有意为之。
能检查一个对象是否拥有某个私有字段吗?
可以,在类内部使用 in 运算符:如果 obj 拥有该私有字段,#field in obj 返回 true。这对品牌检查很有用:在调用方法之前,确认一个对象确实是你的类的实例。