JavaScript 的新特性:类的 #private 字段
原文:https://medium.com/the-thinkmill/javascripts-new-private-class-fields-93106e37647a
作者:James Kyle
这是什么,如何使用,为什么需要?
JavaScript 标准的第二阶段(Stage 2)加入了类私有字段。它还没有最终确定,但 JavaScript 标准委员会认为这个特性会被开发出来并最终纳入标准(虽然它可能还会改变)
它的语法(当前)看起来像这样:
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
的实例,所以我们允许在类定义中去寻找 #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;
console.log(foo.bar); // ``undefined`` (boom... detected again)
这种封装同样对子类有效。子类应该可以拥有相同名称的私有字段,而不必担心父类是否已经存某个私有字段是这个名称。
class Foo {
#fieldName = 1;
}
class Bar extends Foo {
fieldName = 2; // Works!
}
注意:关于封装或“强私有”背后的动机,阅读 FAQ 中这个部分。
那么, 为什么用 # 号[译者注:URL 的 Hash标记]?
很多人想知道“为什么不像其它语言那样使用 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;
}
}
在这些语言中,以同样的方式访问私有字段和公共字段。所以它们才会这样定义。
但是在 JavaScript 中,我们不能使用 this.field
来引用私有属性(稍后深入),我们需要一种基于语法的方法来连接它们的关系。这两个地方使用 #
更能清楚的表明引用的是什么。
什么引用需要 # (Hash 标记)?
因为如下原因,我们需要使用 this.#field
而不是 this.field
:
因为 #封装 (参阅上面“封装”一节),我们要能同时访问公共和私有的同名字段。因此访问一个私有字段不是按常规的方式查找。
JavaScript 中的公共字段可以通过
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
的。为了访问私有字段,我们假设 other
是 Point
类的实例。
因为我们使用了 #
Hash标签 语法,它告诉 JavaScript 编译器我们正在从当前类中寻找私有属性。
那么,如果不用 #
Hash标签,会发生什么呢?
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;
}
更糟糕的是,如果我们引用一个私有属性的话,就不得不在类中对每次属性访问进行这样的检查。
访问属性已经非常慢了,我们绝不想让它变得更慢。
TL;DR: 我们需要对私有属性使用 #
Hash标记,因为不正常的使用标准属性访问机制会导致意外行为,并造成巨大的性能问题。
将私有属性添加到语言中是件非常好的事情。感谢努力工作在 TC39 勤劳的人们,他们正在让这美好的事情发生!
本文分享自微信公众号 - 边城客栈(fancyidea-full)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。