译者按:社区一直以来有一个声音,就是反对使用#
声明私有成员。可是不少质疑的声音过于浅薄、人云亦云。其实 TC39 早就对此类呼声作过回应,而且概括了一篇 FAQ。翻译这篇文章的同时,我会进行必定的扩展(有些问题的描述不够清晰),目的是让你们取得必定的共识。我认为,只有你知其然,且知其因此然,你的质疑才是有力量的。译者按:首先要明确的一点是,委员会对于私有成员不少设计上的抉择是基于 ES 不存在类型检查,为此作了不少权衡和让步。这篇文章在不少地方也会说起这个不一样的基本面。html
#
是怎么回事?#
是 _
的替代方案。java
class A { _hidden = 0; m() { return this._hidden; } }
以前你们习惯使用 _
建立类的私有成员,但这仅仅是社区共识,实际上这个成员是暴露的。git
class B { #hidden = 0; m() { return this.#hidden; } }
如今使用 #
建立类的私有成员,在语言层面上对该成员进行了隐藏。github
因为兼容性问题,咱们不能去改变 _
的工做机制。闭包
译者按:若是将私有成员的语义赋予_
,以前使用_
声明公共成员的代码就出问题了;并且就算你以前使用_
是用来声明私有成员的,你能保证你心中的语义和现阶段的语义彻底一致么?因此为了慎重起见,将以前的一种错误语法(以前类成员以 # 开头会报语法错误,这样保证了之前不存在这样的代码)加以利用,变成私有成员语法。
this.x
访问?译者按:这个问题的意思是,若是类 A 有私有成员 #x( 其中 # 是声明私有,x 才是成员名),为何内部不能经过 this.x 访问该成员,而必定要写成 this.#x?译者按:如下是一系列问题,问题 -> 解答 -> 延伸问题 -> 解答 ...函数
有 x
这个私有成员,不意味着不能有 x
这个公共成员,所以访问私有成员不能是一个普通的查找。测试
这是 JS 的一个问题,由于它缺乏静态类型。静态类型语言使用类型声明区分外部公共/内部私有的状况,而不须要标识符。可是动态类型语言没有足够的静态信息区分这些状况。this
x
,即便父类有一个同名的私有成员。译者按:感受第二点有点文不对题。
其余支持私有成员的语言一般是容许的。以下是彻底合法的 Java 代码:翻译
class Base { private int x = 0; } class Derived extends Base { public int x = 0; }
译者按:所谓的“封装性”(encapsulation / hard private)是很重要的概念, 最底下会有说明。最简单的解释是,外部不能以任意方式获取私有成员的任何信息。假设,公共成员和私有成员冲突,而x
是obj
的私有成员,这时候外部存在obj.x
。若是公私冲突,这里将会报错,外部就嗅探到了obj
存在x
这个私有成员。这就违背了“封装性”。
属性访问的语义已经很复杂了,咱们不想仅仅为了这个特性让每次属性访问都更慢。设计
译者按:属性访问的复杂性能够从 toFastProperties 和 toFastProperties 如何使对象的属性更快 管窥一二
它(运行时检测)还可能让类的方法被非实例(好比普通对象)欺骗,使其在非实例的字段上进行操做,从而形成私有成员的泄漏。这条评论 是一个例子。
译者按:若是不结合以上的例子,上面这句话其实很难理解。因此我以为有必要扩展一下,虽然有不少人认为这个例子没有说服力。
首先我但愿你了解 Java,由于我会拿 Java 的代码作对比。
其次我再明确一下,这个问题的根本在于 ES 没有静态类型检测,而 TS 就不存在此类烦恼。
public class Main { public static void main(String[] args){ A a1 = new A(1); A a2 = new A(2); a1.swap(a2); a1.say(); a2.say(); } } class A { private int p; A(int p) { this.p = p; } public void swap(A a) { int tmp = this.p; this.p = a.p; a.p = tmp; } public void say() { System.out.println(this.p); } }以上的例子是一段正常的 Java 代码,它的逻辑很简单:声明类 A,A 存在一个公共方法,容许实例和另外一个实例交换私有成员 p。
把这部分逻辑转换为 JS 代码,而且使用 private 声明
class A { private p; constructor(p) { this.p = p } swap(a) { let tmp = a.p; a.p = this.p; this.p = tmp; } say() { console.log(this.p); } }乍一看是没有问题的,但 swap 有一个陷阱:若是传入的对象不是 A 的实例,或者说只是一个普通的对象,是否是就能够把私有成员 p 偷出来了?
JS 是不能作类型检查的,那咱们怎么声明传入的 a 必须是 A 的实例呢?现有的方案就是检测在函数体中是否存在对入参的私有成员的访问。好比上例中,函数中若是存在 a.#p,那么 a 就必须是 A 的实例。不然就会报
TypeError: attempted to get private field on non-instance
这就是为何对私有成员的访问必须在语法层面上体现,而不能是简单的运行时检测。
x
时,为何不让 obj.x
老是表明对私有成员的访问?译者按:这个问题的意思是当某个类声明了私有成员x
,那么类中全部的成员表达式sth.x
都表示是对sth
的私有成员x
的访问。我以为这是一个蠢问题,谁同意?谁反对?
类方法常常操做不是实例的对象。当 obj
不是实例的时候,若是 obj.x
忽然间再也不指的是 obj
的公共字段 x
,仅仅是由于在类的某个地方声明了私有成员 x
,那就太奇怪了。
this
关键字特殊的语义?译者按:这个问题针对前一个答案,你说obj.x
不能作这种简单粗暴的处理,那么this.x
能够咯?
this
已是 JS 混乱的缘由之一了;咱们不想让它变的更糟。同时,这还存在一个严重的重构风险:若是 const thiz = this; thiz.x
和 this.x
存在不一样的语义,将会带来很大的困扰。
并且除了 this
,传入的实例的私有成员将没法访问(好比延伸问题 2 的 js 示例中传入的 a)。
this
以外的对象对私有成员的访问?举个栗子,这样一来甚至可使用 x
替代 this.x
表示对私有属性的访问?译者按:这个问题再作了一次延伸,上面提到传入的实例的私有成员不能访问,这个问题是:不能访问就不能访问呗,有什么关系?
这个提案的目的是容许同类实例之间私有属性的互相访问。另外,使用裸标识符(即便用 x
代替 this.x
)不是 JS 的常见作法(除了 with
,而 with
的设计也一般被认为是一个错误)。
译者按:一系列延伸问题到此结束,这类问题弄懂了基本上就掌握私有成员的核心语义和设计原则了。
this.#x
能够访问私有属性,而 this[#x]
不行?私有
的概念。举个栗子:class Dict extends null { #data = something_secret; add(key, value) { this[key] = value; } get(key) { return this[key]; } } new Dict().get("#data"); // 返回了私有属性
this.#x
和 this[#x]
不一样的语义是否破坏了当前语法的稳定性?不彻底是,但这确实是个问题。不过从某个角度上来讲,this.#x
在当前的语法中是非法的,这已经破坏了当前语法的稳定性。
另外一方面,this.#x
和 this[#x]
之间的差别比你看到的还要大,这也是当前提案的不足。
this#x
,把 .
去掉?这是可行的,可是若是咱们再简化为 #x
就会出问题。
译者按:这个说法很简单,我直接列在下面
栗子:
class X { #y z() { w() #y() // 会被解析为w()#y } }
泛言之,由于 this.#
的语义更为清晰,委员会基本都支持这种写法。
译者按:这也是被认为没有说服力的一个说辞,由于委员会把this#x
极端化成了#x
,而后描述#x
的不足,却没有直接给出this#x
的不足。
private x
?这种声明方式是其余语言使用的(尤为是 Java),这意味着使用 this.x
访问该私有成员。
假设 obj
是类实例,在类外部使用 obj.x
表达式,JS 将会静默地建立或访问公共成员,而不是抛出一个错误,这将会是 bug 的主要潜在来源。
它还使声明和访问对称,就像公共成员同样:
class A { pub = 0; #priv = 1; m() { return this.pub + this.#priv; } }
译者按:这里说明了为何使用
#
不使用private
的主要缘由。咱们理一下:若是咱们使用
private
class A { private p; say() { console.log(this.p); } } const a = new A; console.log(a.p); a.p = 1;例子当中,对属性的建立若是不抛错,是否就会建立一个公共字段?
若是建立了公共字段,调用a.say()
打印的是公共字段仍是私有字段?是否是打印哪一个都感受不对?
可能你会说,那就抛错好了?那这样就是运行时检测,这个问题在上面有过描述。
由于这个功能很是有用,举个栗子:判断 Point
是否相等的 equals
方法。
实际上,其余语言因为一样的缘由也是这样设计的;举个栗子,如下是合法的 Java 代码
class Point { private int x = 0; private int y = 0; public boolean equals(Point p) { return this.x == p.x && this.y == p.y; } }
#
?没人说 #
是最漂亮最直观的符号,咱们用的是排除法:
@
是最初的选择,可是被 decorators
占用了。委员会考虑过交换 decorators
和 private
的符号(由于它们都还在提案阶段),但最终仍是决定尊重社区的习惯。_
对现有的项目代码存在兼容问题,由于以前一直容许 _
做为成员变量名的开头。%
, ^
, &
, ?
。考虑到咱们的语法有点独特 —— x.%y
当前是非法的,因此不存在二义性。但不管如何,简写会带来问题。举个栗子,如下代码看上去像是将符号做为中缀运算福:class Foo { %x; method() { calculate().my().value() %x.print() } }
如上,开发人员看上去像是但愿调用 this.%x
上的 print
方法。但实际上,将会执行取余的操做!
最后,惟一的选项是更长的符号序列,但比起单个字符彷佛不太理想。
译者按:委员会仍是举了省略分号时的例子,但是上面也说了,就算是
#
,也一样存在问题。
这样作会违反“封装性”。其余语言容许并非一个充分的理由,尤为是在某些语言(例如 C++)中,是经过直接修改内存实现的,并且这也不是一个必需的功能。
意味着私有成员是彻底内部的:没有任何类外部的 JS 代码能够探测和影响到它们的存在,它们的成员名,它们的值,除非类本身选择暴露他们。(包括子类和父类之间也是彻底封装的)。
意味着反射方法们,好比说 getOwnPropertySymbols 也不能暴露私有成员。
意味着若是一个类有一个私有成员 x
,在类外部实例化类对象 obj
,这时候经过 obj.x
访问的应该是公共成员 x
,而不是访问私有成员或者抛出错误。注意这里的现象和 Java 并不一致,由于 Java 能够在编译时进行类型检查而且禁止经过成员名动态访问内容,除非是反射接口。
WeakMaps
已经能够模拟真实的封装(以下),可是两种方式和类结合都过于浪费,并且还涉及了内存使用的语义,也许这很让人惊讶。此外, 实例闭包的方式还禁止同类的实例间共享私有成员([如上]](#share)),而 WeakMaps
的方式还存在一个暴露私有数据的潜在风险,而且运行效率更低。Symbol
做为属性名实现(以下)。当前提案正在努力推动硬隐私,使 decorators 或者其余机制提供给类一个可选的逃生通道。咱们计划在此阶段收集反馈,以肯定这是不是正确的语义。
查看这个 issue 了解更多。
WeakMap
如何模拟封装?const Person = (function() { const privates = new WeakMap(); let ids = 0; return class Person { constructor(name) { this.name = name; privates.set(this, { id: ids++ }); } equals(otherPerson) { return privates.get(this).id === privates.get(otherPerson).id; } }; })(); let alice = new Person("Alice"); let bob = new Person("Bob"); alice.equals(bob); // false
然而这里仍是存在一个潜在的问题。假设咱们在构造时添加一个回调函数:
const Person = (function() { const privates = new WeakMap(); let ids = 0; return class Person { constructor(name, makeGreeting) { this.name = name; privates.set(this, { id: ids++, makeGreeting }); } equals(otherPerson) { return privates.get(this).id === privates.get(otherPerson).id; } greet(otherPerson) { return privates.get(this).makeGreeting(otherPerson.name); } }; })(); let alice = new Person("Alice", name => `Hello, ${name}!`); let bob = new Person("Bob", name => `Hi, ${name}.`); alice.equals(bob); // false alice.greet(bob); // === 'Hello, Bob!'
乍看好像没有问题,可是:
let mallory = new Person("Mallory", function(name) { this.id = 0; return `o/ ${name}`; }); mallory.greet(bob); // === 'o/ Bob' mallory.equals(alice); // true. 错了!
Symbols
提供隐藏但不封装的属性?const Person = (function() { const _id = Symbol("id"); let ids = 0; return class Person { constructor(name) { this.name = name; this[_id] = ids++; } equals(otherPerson) { return this[_id] === otherPerson[_id]; } }; })(); let alice = new Person("Alice"); let bob = new Person("Bob"); alice.equals(bob); // false alice[Object.getOwnPropertySymbols(alice)[0]]; // == 0,alice 的 id.
译者按:FAQ 到此结束,可能有的地方会比较晦涩,多看几遍写几个 demo 基本就懂了。我以为技术存在看山是山 -> 看山不是山 -> 看山仍是山
这样一个渐进的过程,翻译这篇 FAQ 也并不是为#
辩护,只是如今不少质疑还停留在看山是山
这样一个阶段。我但愿这篇 FAQ 可让你看山不是山
,最后达到看山仍是山
的境界:问题仍是存在问题,不过是站在更全面和系统的角度去思考问题。