翻译,原始文章:“Class-fields-proposal” or “what went wrong in tc39 committee”javascript
一直以来,咱们都指望有一天能在JavaScript中较为简单地使用其余语言常见的封装语法。好比,咱们想要类属性/字段的语法,而且它的实现方式并不会破坏现有的程序。如今看起来,这一天已经到来:在TC39委员会的努力之下,类字段提案已经进入stage 3
,甚至已经被Chrome实现html
老实说,我很乐意写一篇文章,描述为何您必须使用这个新功能以及如何实现它。但惋惜我没法这么作。vue
参考文档在此不赘述了,具体参考:原始说明,FAQ,规范变动。java
类字段说明和用法:git
class A {
x = 1;
method() {
console.log(this.x);
}
}
复制代码
从外部代码访问字段:github
const a = new A();
console.log(a.x);
复制代码
一眼看去稀松日常,有些人可能会说咱们在Babel和TypeScript中这样使用多年了。chrome
但有一件事值得注意:这个语法使用[[Define]]
语义而不是咱们习惯的[[Set]]
语义。这意味着实际上上面的代码不等价于如下用法:typescript
class A {
constructor() {
this.x = 1;
}
method() {
console.log(this.x);
}
}
复制代码
而等价于下述用法:npm
class A {
constructor() {
Object.defineProperty(this, "x", {
configurable: true,
enumerable: true,
writable: true,
value: 1
});
}
method() {
console.log(this.x);
}
}
复制代码
尽管在这个例子下,两种用法实际表现几乎没有什么区别,但实际有一个很重要的区别。咱们假设咱们有一个像这样的父类:编程
class A {
x = 1;
method() {
console.log(this.x);
}
}
复制代码
从该父类派生出一个子类以下:
class B extends A {
x = 2;
}
复制代码
而后使用:
const b = new B();
b.method(); // prints 2 to the console
复制代码
而后为了某些(不重要的)缘由,咱们以一种彷佛向后兼容的方式改变了A类:
class A {
_x = 1; // for simplicity let's skip that public interface got new property here
get x() { return this._x; };
set x(val) { return this._x = val; };
method() {
console.log(this._x);
}
}
复制代码
对于[[Set]]
语义,它确实是向后兼容的。 可是对于[[Define]]
不是。 如今调用b.method()
会将打印1
而不是2
到控制台。缘由是在Object.defineProperty
的做用下,不会调用A
类声明的属性描述符以及其getter/setter。 所以,在派生类中,咱们以相似变量词法做用域的方式隐藏了父类x
性:
const x = 1;
{
const x = 2;
}
复制代码
咱们可使用no-shadowed-variable
/no-shadow
这样的liner规则帮助咱们检测常见的词法做用域变量隐藏。 可是不幸的是,不太可能有人会建立no-shadowed-class-field
这样的规则帮助咱们规避类字段的隐藏。
尽管如此,我并非[[Define]]
语义的的坚决反对者(尽管我更喜欢[[Set]]
语义),由于它有它的好的优势。然而,它的优势并无超过主要的缺点——咱们多年来一直使用[[Set]]
语义,由于它是babel6
和TypeScript
的默认行为。
我不得不强调一下,
babel7
改变了默认行为。
咱们来看看这个提案中最具争议的部分。 它是如此有争议:
stage 3
;声明私有字段的语法:
class A {
#priv;
}
复制代码
并使用如下表示法访问:
class A {
#priv = 1;
method() {
console.log(this.#priv);
}
}
复制代码
这个语法看起来违反直觉,而且很不直观(this.#priv != this['#priv']
),而且没有使用JavaScript的保留字privaye
/protected
(这可能会让已经使用TypeScript的开发者感到伤脑筋),而且为更多的访问级别的设计留下隐患。在这样的情境下,我深刻的调查并参与了相关讨论。
若是这仅仅与语法形式有关,即主观审美上咱们难以接受,那么最后咱们或许能够忍受这样的语法并习惯它。 可是,还有一个语义问题……
让咱们来看看现有提案背后的语义。 咱们可以在没有新语法可是保持原有行为的状况下重写前面的示例:
const privatesForA = new WeakMap();
class A {
constructor() {
privatesForA.set(this, {});
privatesForA.get(this).priv = 1;
}
method() {
console.log(privatesForA.get(this).priv);
}
}
复制代码
顺便说一句,一名委员会成员使用这种语义建立了一个小型实用程序库,这使咱们如今就可使用私有状态。 他的目标是代表这种功能被委员会高估了。其格式化代码只有27行。
很棒,咱们能够拥有硬私有
了,没法从外部代码访问/拦截/跟踪内部的字段,同时咱们甚至能够经过如下方式访问同一类的另外一个实例的私有:
isEquals(obj) {
return privatesForA.get(this).id === privatesForA.get(obj).id;
}
复制代码
这一切都很是方便,除了这个语义不只包括封装
,还包括brand-checking
(您没必要谷歌这个术语,由于您不太可能找到任何相关的信息)。 brand-checking
与鸭子类型
相反,从某种意义上说,它根据特定代码肯定特定对象而非根据该对象的公共接口肯定对象。 实际上,这种检查有其本身的用途——在大多数状况下,它们与在同一进程中安全执行不受信任的代码有关,能够直接共享对象而无需序列化/反序列化开销。
可是一些工程师坚持认为这是正确封装的要求。
尽管这是一个很是有趣的可能实现,它涉及膜
模式(简短和详尽描述),Realms
提案和Mark Samuel Miller的计算机科学研究(他也是委员会成员),可是根据个人经验,它彷佛并不常见于大多数开发人员的平常工做中。
brand-checking
的问题正如我以前所说,brand-checking
与鸭子类型相反。 在实践中,这意味着使用如下代码:
const brands = new WeakMap();
class A {
constructor() {
brands.set(this, {});
}
method() {
return 1;
}
brandCheckedMethod() {
if (!brands.has(this)) throw 'Brand-check failed';
console.log(this.method());
}
}
复制代码
brandCheckedMethod
只能被A
的实例调用,即便target符合此类的全部结构,此方法也会抛出异常:
const duckTypedObj = {
method: A.prototype.method.bind(duckTypedObj),
brandCheckedMethod: A.prototype.brandCheckedMethod.bind(duckTypedObj),
};
duckTypedObj.method(); // no exception here and method returns 1
duckTypedObj.brandCheckedMethod(); // throws an exception
复制代码
显然,这个例子是刻意设计的,而且这种鸭子类型
的好处是值得怀疑的。除非咱们谈论Proxy
。 代理有一个很是重要的使用场景——元编程。 为了使代理执行全部必需的有用工做,代理包装的对象的方法应该在代理的上下文中调用,而不是在目标的中调用:
const a = new A();
const proxy = new Proxy(a, {
get(target, p, receiver) {
const property = Reflect.get(target, p, receiver);
doSomethingUseful('get', retval, target, p, receiver);
return (typeof property === 'function')
? property.bind(proxy) // actually bind here is redundant, but I want to focus your attention on method's context
: property;
}
});
复制代码
调用proxy.method(); 将致使作一些在代理中声明并返回1
的有用工做,当调用proxy.brandCheckedMethod();
而不是作一些有用的工做两次将致使抛出异常,由于a !== proxy
而且brand-check
失败了。
固然,咱们能够在真实目标而不是代理的上下文中执行方法/函数,而且在某些状况下它就足够了(例如实现膜
模式),但它并不是对于全部状况都是够用的(例如,实现反应式属性:MobX 5已经使用代理实现,Vue.js和Aurelia正在试验这种方法以便用于将来版本)。
一般,虽然brand-check
须要显式声明,但这并非问题:开发人员只需选择他/她须要哪一种权衡以及缘由。 在明确的brand-check
的状况下,它能够以容许其与某些可信代理进行交互的方式实现。
不幸的是,目前的提案没有给予这种灵活性:
class A {
#priv;
method() {
this.#priv; // brand-check ALWAYS happens here
}
}
复制代码
若是在没有用A
的构造函数构建的对象的上下文中调用method
方法,该方法将始终抛出异常。这就是最可怕的事实:brand-check
在这里隐含并与另外一个特征——“封装”混合。
虽然几乎全部类型的代码都须要封装,但品牌检查的用例数量很是有限。 当开发人员想要隐藏实现细节时,将它们混合成一种语法将致使意外的brand-check
,而为了推广这个proposal,宣传#是新的_
更是雪上加霜。
您还能够阅读有关当前提案破坏代理行为的讨论细节。 在Aurelia开发者和Vue.js做者参与其中。此外,个人评论描述了代理的几个用例之间的差别,这可能颇有趣。 并讨论了私有字段与
膜
模式之间的关系。
除非有其余选择,不然全部这些讨论都没有多大意义。 不幸的是,它们都没有达到第一阶段,所以这些备选提案没有机会获得充分发展。 可是,我想指出其中的一些,以某种方式解决上述问题。
private
做为对象使用看起来我彷佛在责怪委员会——但实际上,我没有。 我只是认为,为了在JS中实现适当的封装,已经通过多年(甚至几十年,取决于选择的起点)的努力,而咱们业界更是的发生了不少变化,可能委员会错过了一些变化。致使,相关事体的优先级别可能会变得有些模糊。
不光如此,咱们做为一个社区,迫使TC39更快地发布功能,可是咱们却没有为早期提案提供足够的反馈,结果致使争论不少而可以用来改变某些事情的时间不多。
有观点认为,在这种状况下,该提案过程失败了。
在长期潜水以后,我决定尽我所能,以防止未来发生这种状况。 不幸的是,我作不了多少——只能写写评论文章并在babel
中实现早期提案。
总的来讲,反馈和沟通是最重要的,因此我恳请你们与委员会分享更多的想法。