它们(指类的私有字段)是什么,它们怎么工作,以及它们为什么是这样

译注:类的私有字段分为私有方法和私有属性,但原文没有明确区分。为了理解方便,本译文进行了明确区分。

这首歌 “Noise Pollution” — Portugal. The Man 和本文更配哦!

正文

类的私有字段(private class fields,下同) 这个 JavaScript 语言新特性的提案已经处在第二阶段了(Stage 2)(译注:JavaScript 语言新特性的演进流程)。 虽然这个提案还没完全确定下来,但是 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;
  }
}

这个语法有两个重点:

  • 定义私有属性
  • 使用(referencing)私有属性

定义私有属性

定义私有属性和定义公共属性(defining public fields)几乎一样:

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

fooFoo 的一个实例,所以我们可以在 Foo 内部内查找到 #privateValue

类的私有方法

类的私有字段的提案作为 类字段的提案 的一部分,目的只是完善类的相关特性,不会对现有类的语法做任何修改。 类的私有方法的提案会紧跟着 类的私有字段提案,它的语法看起来是这样的:

class Foo {
  constructor() {
    this.#method();
  }
  #method() {
    // ...
  }
}

同时,你也可以把函数赋值给私有属性:

class Foo {
  constructor() {
    this.#method();
  }

  #method = () => {
    // ...
  };
}

封装(Encapsulation)

如果你声明了一个类的实例,你不能用实例去使用类的私有字段。你只能在类定义的内部去使用这些私有字段。

class Foo {
  #bar;
  method() {
    this.#bar; // Works
  }
}
let foo = new Foo();
foo.#bar; // Invalid!

更进一步讲,为了达到真正的私有化(truly private),你甚至不能够检测一个私有字段是否存在。 为了达到这个目的,我们需要将 #propertyproperty 认为是同一个属性名。

class Foo {
  bar = 1; // public bar
  #bar = 2; // private bar
}

如果 #propertyproperty 不是同一个字段名,你就可以通过 给共有字段赋值 来达到 检验是否存在私有字段 的目的。

如下所示:

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

为什么用 #hashtag

有很多人会有疑惑,说:为什么不像其它语言一样,用 private 来当私有字段的关键字?

举例来说明来说明为什么用 #hashtag 这个语法:

class Foo {
  private value;

  equals(foo) {
    return this.value === foo.value;
  }
}

我们分开来看:

为什么不用声明私有字段不使用 private

有很多其它的语言用 private 来声明类的私有字段。写出来的代码是这样的:

class EnterpriseFoo {
  public bar;
  private baz;
  method() {
    this.bar;
    this.baz;
  }
}

这些用 private 来声明类私有字段的语言,对于公共字段和私有字段的存取方式是一样的。所以它们这样定义是没有任何问题的。

但是说到 JavaScript,我们不能把 this.field 这种获取公共字段的方式应用到私有字段上(接下来会讲到)。我们需要能够从语法层次上来解决共有和私有两者关系的方法。 而通过使用 # 就能够很清晰的解决。

为什么用 #hashtag

我们用 this.#field 取代 this.field 有以下几点原因:

  • 因为封装性(encapsulation)的要求,我们不能允许私有字段和公共字段有一样的名字。所以获取私有属性不能用 this.field 这种方式。
  • 获得公共属性可以通过 this.fieldthis['field'] 这两种方式。 但是私有属性不支持第二种语法,不仅因为私有属性需要是静态的(need to be static)而且第二种语法也会造成误解。
  • 需要更多的校验代价:

如下面的例子所示:

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.#xother.#y。通过例子里的这种方式来获取私有字段,我们可以很清楚的知道 otherPoint 的实例。 因为我们用了 # 来告诉 JavaScript 编译器,我们正在从当前类中查找私有属性。

如果我们不用 #hashtag ,将会怎样呢?

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

我们需要 #hashtag 来处理私有字段,因为使用标准的属性获取方式会导致意外的情况,也会导致性能问题。

Private fields are an awesome addition to the language. Thanks to all the wonderful hardworking people on TC39 who made/are making them happen!

原文地址:https://medium.com/the-thinkmill/javascripts-new-private-class-fields-93106e37647a

以上。