“ Class-fields-proposal”或“ tc39 commit出了什么问题”

很久以前,我们所有人都希望在JS中进行常规封装,而无需不必要的手势即可使用它。 我们还需要方便的构造来声明类属性。 最后,我们希望语言中的所有这些功能都以不破坏现有应用程序的方式出现。


看来这是幸福: class-fields-proposal ,经过tc39委员会多年的折磨,它仍然进入了stage 3 ,甚至在chrome中得以实现


老实说,我真的很想写一篇关于为什么要使用新语言功能以及如何使用它的文章,但是不幸的是,这篇文章根本不是关于那方面的。


当前缺失的描述


在此,我将不再重复原始说明常见问题解答规格更改 ,而仅简要概述要点。


类字段


声明字段并在类中使用它们:


 class A { x = 1; method() { console.log(this.x); } } 

访问课程外的字段:


 const a = new A(); console.log(ax); 

一切似乎都是显而易见的,多年来,我们一直在通过BabelTypeScript使用这种语法。


只有细微的差别。 这种新语法使用[[Define]] ,而不是[[Set]]我们一直以来所使用[[Set]]语义。


实际上,这意味着上面的代码不相等


 class A { constructor() { this.x = 1; } method() { console.log(this.x); } } 

但实际上这等效于此:


 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(); //   2   

然后,由于某种原因,以一种似乎向后兼容的方式更改了A类:


 class A { _x = 1; //  ,   ,        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规则的短绒将拯救我们,但是有人做出no-shadowed-class-field的可能性趋于零。


顺便说一句,我将为俄罗斯带shadowed术语获得成功表示感激。

尽管有上述所有内容,但我并不是新语义的不可接受的反对者(尽管我更喜欢另一种),因为它有其积极的方面。 但是,不幸的是,这些优点并没有超过最重要的缺点-我们使用[[Set]]语义已经很多年了,因为默认情况下在babel6TypeScript使用了[[Set]]语义。


没错,值得注意的是,在babel7 默认值已更改

有关此主题的更多原始讨论可以在此处此处阅读。


私人领域


现在,我们将继续讨论这一最具争议的部分。 有争议的是:


  1. 尽管事实上该功能已在Chrome Canary中实现,并且默认情况下已启用公共字段,但私有字段仍处于滞后状态;
  2. 尽管私有字段初始prozal已与当前字段合并,但仍在创建请求以分离这两个功能(例如, one2,34 );
  3. 尽管有第三阶段,甚至一些委员会成员(如艾伦•维尔夫斯-布罗克Allen Wirfs-Brock)凯文•史密斯Kevin Smith ))都表示了自己的看法 ,并提出了其他选择 ;
  4. 这错过了一个创下记录数量的记录- 当前版本库中的 129个 + 原始 版本中的 96个 ,而BigInt版本中则为126个 ,记录持有人大多持负面评论 ;
  5. 我不得不创建一个单独的线程 ,试图以某种方式总结针对它的所有主张。
  6. 我不得不写一个单独的常见问题 ,涵盖这部分
    但是,由于争论不力,出现了这样的讨论(
  7. 我个人花了很长时间(有时是工作时间)来弄清所有事情,甚至找到解释为什么他会那样或提供合适的选择
  8. 最后,我决定写这篇评论文章。

专用字段声明如下:


 class A { #priv; } 

并访问它们如下:


 class A { #priv = 1; method() { console.log(this.#priv); } } 

我什至不会提出这样一个问题:其背后的思维模型不是很直观( this.#priv !== this['#priv'] ),不使用已经保留的private / protected单词(这必将引起额外的痛苦)对于TypeScript开发人员而言),目前尚不清楚如何将其扩展为其他访问修饰符 ,而且语法本身也不是很漂亮。 尽管所有这些都是促使我进行更深入研究和参与讨论的原始原因。


所有这些都与语法有关,其中主观审美偏好非常强烈。 一个人可以忍受它,并随着时间的推移习惯它。 如果不是一件事:存在一个非常重要的语义问题...


语义WeakMap


让我们看一下现有主张的背后是什么。 我们可以使用封装而不使用新的语法来重写上面的示例,但是保留当前语法的语义:


 const privatesForA = new WeakMap(); class A { constructor() { privatesForA.set(this, {}); privatesForA.get(this).priv = 1; } method() { console.log(privatesForA.get(this).priv); } } 

顺便说一句,在这种语义的基础上,一个委员会成员甚至构建了一个小型实用程序库 ,该允许您现在使用私有状态,以表明该功能被委员会高估了。 格式化的代码仅占用27行。

总的来说,一切都很好,我们得到了hard-private ,它不能以任何方式从外部代码获取/拦截/跟踪,同时我们可以访问同一类的另一个实例的私有字段,例如:


 isEquals(obj) { return privatesForA.get(this).id === privatesForA.get(obj).id; } 

嗯,这非常方便,除了以下事实外,除了封装本身之外,此语义还包括brand-checking (您不能用谷歌搜索它是什么-您不太可能找到相关信息)。
brand-checking与“ duck-typing ”相反,因为它不检查对象的公共接口,而是检查对象是使用可信代码构建的事实。
实际上,这种检查具有一定的范围-它主要与在具有受信任地址的单个地址空间中调用不受信任代码的安全性以及不进行序列化而直接交换对象的能力相关。


尽管有些工程师认为这是正确封装的必要部分。

尽管这是一个非常奇怪的机会,但根据我的经验,这与模式( 简短较长的描述),计算机科学领域的领域宣传和科学工作密切相关, 马克·塞缪尔·米勒Mark Samuel Miller )也是该委员会的成员(也是委员会成员) ,在大多数开发人员的实践中,这种情况几乎不会发生。


顺便说一句,当我重写vm2以满足自己的需求时,我仍然遇到了麻烦(尽管当时我不知道那是什么)。

brand-checking问题


如前所述, brand-checkingduck-typing相反。 实际上,这意味着拥有以下代码:


 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()); } } 

只能使用A类的实例调用brandCheckedMethod A即使目标是保留此类不变性的对象,此方法也会引发异常:


 const duckTypedObj = { method: A.prototype.method.bind(duckTypedObj), brandCheckedMethod: A.prototype.brandCheckedMethod.bind(duckTypedObj), }; duckTypedObj.method(); //        1 duckTypedObj.brandCheckedMethod(); //      

显然,此示例是完全综合的,在我们考虑Proxy之前,像这样使用duckTypedObj值得怀疑。
元编程是非常重要的代理使用方案之一。 为了使代理能够完成所有必要的有用工作,使用代理包装的对象的方法必须在代理的上下文中而不是在目标的上下文中执行,即:


 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) : property; } }); 

调用proxy.method(); 在调用proxy.brandCheckedMethod();将完成代理中声明的有用工作并返回1 proxy.brandCheckedMethod(); 而不是从代理执行两次有用的工作,它会引发异常,因为a !== proxy ,这意味着未通过brand-check


是的,我们可以在实际目标(而不是代理)的上下文中执行方法/函数,并且在某些情况下,这已经足够(例如,实现模式),但是对于所有情况,这还不够(例如,实现反应性: MobX 5已经使用了代理)为此, Vue.jsAurelia正在针对将来的版本尝试这种方法。


通常,只要需要明确进行brand-check ,这就不成问题-开发人员只需要有意识地决定要进行哪种折衷以及是否需要进行折衷,此外,在进行明确的brand-check的情况下brand-check您可以采用以下方式实施该错误不会抛出给受信任的代理。


不幸的是,当前的做法剥夺了我们这种灵活性:


 class A { #priv; method() { this.#priv; //    brand-check   } } 

如果未在使用构造函数A构造的对象的上下文中调用此method它将始终引发异常A 最糟糕的是, brand-check在这里是隐式的,并且与其他功能(封装)混合在一起。


尽管几乎是所有代码所必需的,但brand-check范围相当狭窄。 当开发人员只打算隐藏实现细节时,将它们组合为一种语法将导致以下事实:在用户代码中会出现许多无意的brand-check
而用于推广此目标的口号是# is the new _只会加剧局势。


您还可以阅读有关现有代理如何破坏代理详细讨论Aurelia开发人员之一作者Vue.js在讨论中发表了讲话

另外, 我的评论 (其中更详细地描述了不同代理场景之间的差异)可能对某人来说很有趣。 总体而言, 整个讨论都涉及私有领域和膜的联系

替代品


如果没有其他选择,所有这些讨论将毫无意义。 不幸的是,甚至没有一个替代品进入阶段1 ,结果甚至没有机会得到足够的锻炼。 但是,我将在此处列出以某种方式解决上述问题的替代方案。


  1. Symbol.private-委员会的候补百忧解之一。
    1. 它解决了上述所有问题(尽管可能有其自身的问题,但是鉴于缺乏积极的工作,因此很难找到它们)
    2. 由于缺少集成的brand-check ,膜模式存在问题(尽管 +提供了适当的解决方案)以及缺乏方便的语法,因此再次在委员会上次会议上被撤回
    3. 方便的语法可以建立在实际语法的基础上,如我在此处此处所示
  2. 1.1类 -同一作者的早期posozal
  3. 使用私有作为对象

而不是结论


用文章的语气,似乎我谴责了委员会,但事实并非如此。 在我看来,多年来(取决于起点,甚至可能是几十年),委员会致力于JS封装,行业中的许多事情已经改变,外观可能会模糊,从而导致错误的优先级排序。


此外,我们作为一个社区, 推动tc39迫使他们更快地发布功能,同时在prozos的早期阶段提供很少的反馈,只有在几乎不能改变的时候才使我们愤慨。


据认为 ,在这种情况下,该过程完全失败了。


将其投入脑海并与一些代表交谈后,我决定尽我所能防止类似情况的再次发生-但我可以做些什么(写一篇评论文章,使babel以及所有内容都错过了stage1的实现)。


但是最重​​要的是反馈-所以我想请您参加这项小型调查。 反过来,我将尝试将其传达给委员会。

Source: https://habr.com/ru/post/zh-CN428119/


All Articles