“JavaScript的成员字段提案”或“TC39委员出了什么问题?”
栏目: JavaScript · 发布时间: 5年前
内容简介:一直以来,我们都期望有一天能在JavaScript中较为简单地使用其他语言常见的封装语法。比如,我们想要类属性/字段的语法,并且它的实现方式并不会破坏现有的程序。现在看起来,这一天已经到来:在TC39委员会的努力之下,老实说,我很乐意写一篇文章,描述为什么您必须使用这个新功能以及如何实现它。但可惜我无法这么做。参考文档在此不赘述了,具体参考:
一直以来,我们都期望有一天能在JavaScript中较为简单地使用其他语言常见的封装语法。比如,我们想要类属性/字段的语法,并且它的实现方式并不会破坏现有的程序。现在看起来,这一天已经到来:在TC39委员会的努力之下, 类字段提案
已经进入 stage 3
,甚至已经被Chrome实现
老实说,我很乐意写一篇文章,描述为什么您必须使用这个新功能以及如何实现它。但可惜我无法这么做。
当前提案说明
参考文档在此不赘述了,具体参考: 原始说明 , FAQ , 规范变更 。
类字段
类字段说明和用法:
class A { x = 1; method() { console.log(this.x); } } 复制代码
从外部代码访问字段:
const a = new A(); console.log(a.x); 复制代码
一眼看去稀松平常,有些人可能会说我们在Babel和TypeScript中这样使用多年了。
但有一件事值得注意:这个语法使用 [[Define]]
语义而不是我们习惯的 [[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(); // 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
改变了默认行为。
私有字段
我们来看看这个提案中最具争议的部分。 它是如此有争议:
- 尽管事实上,它已经在Chrome Canary中实现,并且默认情况下公共字段可用,但是私有字段功能仍需额外开启;
- 尽管事实上, 原始的私有字段提案 与当前的提案合并,关于分离私有和公有字段的issue一再出现(如: 140 , 142 , 144 , 148 );
-
甚至一些委员会成员(如: Allen Wirfs-Brock
和 Kevin Smith
)也反对它并提供替代方案,但是该提案仍然顺利进入
stage 3
; - 该提案的issue数量最多—— 当前提案 的GitHub仓库为131个, 原始提案 (合并前)的GitHub仓库为96个(相比 BigInt 提案的issue数量为126个),并且大多数issue持 反对观点 ;
- 甚至创建了 单独的issue ,以便统计总结对它的反对意见;
- 为了证明这一部分的合理性而创建了 单独的FAQ ,然而不够强力论据又导致了新的争论( 133 , 136 )
- 就我个人而言,几乎花了我所有的空闲时间(有时甚至是工作时间),花了大精力试图对其进行调查,充分了解其背后的局限性和决策,弄明白其形成现状的原因,并提出可行的替代方案;
- 最后,我决定写这篇评论文章。
声明私有字段的语法:
class A { #priv; } 复制代码
并使用以下表示法访问:
class A { #priv = 1; method() { console.log(this.#priv); } } 复制代码
这个语法看起来违反直觉,并且很不直观( this.#priv != this['#priv']
),并且没有使用JavaScript的保留字 privaye
/ 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行。
很棒,我们可以拥有 硬私有
了,无法从外部代码访问/拦截/跟踪内部的字段,同时我们甚至可以通过以下方式访问同一类的另一个实例的私有:
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作者
参与其中。此外, 我的评论
描述了代理的几个用例之间的差异,这可能很有趣。 并
讨论了私有字段与 膜
模式之间的关系
。
备选方案
除非有其他选择,否则所有这些讨论都没有多大意义。 不幸的是,它们都没有达到第一阶段,因此这些备选提案没有机会得到充分发展。 但是,我想指出其中的一些,以某种方式解决上述问题。
- Symbol.private ——来自其中一名委员会成员的替代提案。
- Classes 1.1 - 来自同一作者的较早提议
-
把
private
作为对象使用
结论
看起来我似乎在责怪委员会——但实际上,我没有。 我只是认为,为了在JS中实现适当的封装,已经经过多年(甚至几十年,取决于选择的起点)的努力,而我们业界更是的发生了很多变化,可能委员会错过了一些变化。导致,相关事体的优先级别可能会变得有些模糊。
不光如此,我们作为一个社区,迫使 TC39 更快地发布功能,但是我们却没有为早期提案提供足够的反馈,结果导致争论很多而能够用来改变某些事情的时间很少。
有 观点 认为,在这种情况下,该提案过程失败了。
在长期潜水之后,我决定尽我所能,以防止将来发生这种情况。 不幸的是,我做不了多少——只能写写评论文章并在 babel
中实现早期提案。
总的来说,反馈和沟通是最重要的,所以我恳请大家与委员会分享更多的想法。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- JavaScript的类字段声明(提案)
- [译] ECMAScript 的 Observables 提案
- Go 1.15 的提案
- 实用!最新的几个 Vue 3 重要特性提案
- 实用!最新的几个 Vue 3 重要特性提案
- FIBOS 超级节点选举以及提案多签介绍
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Head First 设计模式(中文版)
弗里曼 / O'Reilly Taiwan公司 / 中国电力出版社 / 2007-9 / 98.00元
《Head First设计模式》(中文版)共有14章,每章都介绍了几个设计模式,完整地涵盖了四人组版本全部23个设计模式。前言先介绍这本书的用法;第1章到第11章陆续介绍的设计模式为Strategy、Observer、Decorator、Abstract Factory、Factory Method、Singleton,Command、Adapter、Facade、TemplateMethod、I......一起来看看 《Head First 设计模式(中文版)》 这本书的介绍吧!