【译】JS新语法:私有属性

【译】JS新语法:私有属性

原文:JavaScript's new #private class fields

原作者:thejameskyle

翻译:百度外卖FE - 安秦

译者注

这个语法是框架开发者们非常期待的,这样就可以有效区分用户的命名空间和框架内部字段。

这个语法虽然已经到了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;
  }
}

这个语法有两个关键部分:

  1. 定义私有字段
  2. 引用私有字段

定义私有字段

定义私有字段跟定义公有字段区别不大:

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有以下几个原因:

  1. 为了封装性(见上面封装性章节),我们需要公有和私有字段可以同时拥有相同的名字。所以访问一个私有字段不能是个普通的查询。
  2. JS里公有属性可以通过this.field或者this['field']访问。然而私有属性不支持第二个语法(因为它必须是静态的),这会可能导致混淆。
  3. 你需要付出不少性能带价来做类型检查:

来看看一个代码例子:

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什么都可能是。

这就产生问题了:

  1. 我们的函数根据传入参数的不同会有不同的表现:有时会访问一个私有属性,又有时访问共有属性。
  2. 我们每次都需要检查otherPoint的类型:
if (
  otherPoint instanceof Point &&
  isNotSubClass(otherPoint, Point)
) {
  return getPrivate(otherPoint, 'foo');
} else {
  return otherPoint.foo;
}

更糟糕的是,我们每一个属性访问都需要检查一下是不是在引用私有属性。

属性访问已经挺慢的,所以我们真的不想再给它增加负担了。

上面太长;不看:我们需要用 # 来使用私有属性是因为如果不用,会产生不可预料的表现并且造成巨大的性能影响。

结语

私有属性是对语言的一个帅气加强。感谢所有为TC39辛勤工作的人们让这成为现实。

编辑于 2017-06-15

文章被以下专栏收录