译者注
这个语法是框架开发者们非常期待的,这样就可以有效区分用户的命名空间和框架内部字段。
这个语法虽然已经到了stage-2但是提出时间还比较短,对应的Babel插件还没有发布,所以还不能实际使用,可以先了解语法规则。
译文
类私有字段已经处于JavaScript规范流程的Stage 2,它还没完成但是JS规范协会期望这个功能得到实现并归入语言规范(虽然可能还有变数)。
该语法(目前)如下:
class Point {
#x;
#y;
constructor(x, y) {
this.#x = x;
this.#y = y;
}
equals(point) {
return this.#x === point.#x && this.#y === point.#y;
}
}
这个语法有两个关键部分:
- 定义私有字段
- 引用私有字段
定义私有字段
定义私有字段跟定义公有字段区别不大:
class Foo {
publicFieldName = 1;
#privateFieldName = 2;
}
欲引用一个私有字段,必须先定义它,所以如果你不想在定义的时候给初始值,可以这样:
class Foo {
#privateFieldName;
}
引用私有字段
引用一个私有字段跟引用其他字段类似,只是有一个特殊的语法。
class Foo {
publicFieldName = 1;
#privateFieldName = 2;
add() {
return this.publicFieldName + this.#privateFieldName;
}
}
this.#还有个简写方法:
method() {
#privateFieldName;
}
其实跟这个效果相同:
method() {
this.#privateFieldName;
}
引用实例的私有字段
引用私有字段并不局限于this,你还可以访问同类其他实例里的私有字段:
class Foo {
#privateValue = 42;
static getPrivateValue(foo) {
return foo.#privateValue;
}
}
Foo.getPrivateValue(new Foo()); // >> 42
在这里,foo是Foo的实例,所以可以在Foo类的定义里面访问foo.#privateValue。
私有方法(即将到来?)
私有字段提案只是关注于添加类字段,该提案没有对类方法有任何改动,所以类私有方法即将出现在一个跟进提案当中,并且很可能长这样:
class Foo {
constructor() {
this.#method();
}
#method() {
// ...
}
}
在这之前,你可以给私有字段复制函数值:
class Foo {
constructor() {
this.#method();
}
#method = () => {
// ...
};
}
封装性
如果你在使用一个类的实例,你不能引用该类的私有字段。你只能在该类的定义里引用它们。
class Foo {
#bar;
method() {
this.#bar; // Works
}
}
let foo = new Foo();
foo.#bar; // Invalid!
为了真正地体现私有,你不应该有办法检测一个私有字段是否存在。
为了保证你无法检测到一个私有字段,我们需要私有字段和公有自动可以重名。
class Foo {
bar = 1; // public bar
#bar = 2; // private bar
}
不然如果不允许同名,你就可以用如下方法检测到私有字段是否存在:
foo.bar = 1; // Error: `bar` is private! (boom... detected)
或者静默的版本:
foo.bar = 1;
foo.bar; // `undefined` (boom... detected again)
这种封装性对于子类也同样试用。一个子类应当可以定义同名字段而不用担忧会影响父类型。
class Foo {
#fieldName = 1;
}
class Bar extends Foo {
fieldName = 2; // Works!
}
那为什么是井号?
许多人会想:“为啥不按照其他语言的约定,用一个private关键字呢?”
这有一个这种语法的样例:
class Foo {
private value;
equals(foo) {
return this.value === foo.value;
}
}
我们来分别看看这种语法的两个部分。
为什么定义的时候不用private关键字?
在很多语言里都会使用private来定义私有字段。
这种语言的语法如:
class EnterpriseFoo {
public bar;
private baz;
method() {
this.bar;
this.baz;
}
}
这些语言当中,公有和私有字段的访问方式是一样的。所以这么定义可以理解。
然而在JS当中,因为我们不能用this.field访问私有属性(我一会再讲),我们就需要一种方法进行语法层面的关联。通过在两处都使用#,到底引用了什么就很明显了。
为什么引用的时候需要用 #井号 ?
我们必须用this.#field而不是this.field有以下几个原因:
- 为了封装性(见上面封装性章节),我们需要公有和私有字段可以同时拥有相同的名字。所以访问一个私有字段不能是个普通的查询。
- JS里公有属性可以通过this.field或者this[‘field’]访问。然而私有属性不支持第二个语法(因为它必须是静态的),这会可能导致混淆。
- 你需要付出不少性能带价来做类型检查:
来看看一个代码例子:
class Point {
#x;
#y;
constructor(x, y) {
this.#x = x;
this.#y = y;
}
equals(other) {
return this.#x === other.#x && this.#y === other.#y;
}
}
注意我们是如何引用other.#x以及other.#y。使用私有字段,我们就在假定该实例是我们Point类的一个实例。
因为我们使用了 # 语法,所以也就告知了JS编译器我们是在当前类里的私有属性。
如果我们没用 # 会怎样?
equals(otherPoint) {
return this.x === otherPoint.x && this.y === otherPoint.y;
}
我们有个问题:我们如何知道otherPoint是什么?
JavaScript没有一个静态类型系统,所以otherPoint什么都可能是。
这就产生问题了:
- 我们的函数根据传入参数的不同会有不同的表现:有时会访问一个私有属性,又有时访问共有属性。
- 我们每次都需要检查otherPoint的类型:
if (
otherPoint instanceof Point &&
isNotSubClass(otherPoint, Point)
) {
return getPrivate(otherPoint, 'foo');
} else {
return otherPoint.foo;
}
更糟糕的是,我们每一个属性访问都需要检查一下是不是在引用私有属性。
属性访问已经挺慢的,所以我们真的不想再给它增加负担了。
上面太长;不看:
我们需要用 # 来使用私有属性是因为如果不用,会产生不可预料的表现并且造成巨大的性能影响。
结语
私有属性是对语言的一个帅气加强。感谢所有为TC39辛勤工作的人们让这成为现实。