在实现JavaScript的语言功能时,实现者必须对规范中的语言如何映射到实现中做出决定。有时这相当简单,规范和实现可以共享许多相同的术语和算法。其他时候,实现中的压力使其更具挑战性,要求或迫使实现策略与语言规范相背离。
至少在SpiderMonkey--为Firefox提供动力的JavaScript引擎中,私有字段就是规范语言和实施现实相背离的一个例子。为了了解更多,我将解释什么是私有字段,几个思考它们的模型,并解释为什么我们的实现与规范语言有分歧。
私有字段
私有字段是通过TC39 提案过程添加到JavaScript语言中的一个语言特性,作为类字段提案的一部分,它处于TC39过程的第四阶段。我们将在Firefox 90中推出私有字段和私有方法。
私有字段提案为语言添加了一个严格的 "私有状态 "的概念。在下面的例子中,#x 只能被类A 的实例访问。
class A {
#x = 10;
}
这意味着在该类之外,不可能访问该字段。不像公共字段那样,例如下面的例子所示。
class A {
#x = 10; // Private field
y = 12; // Public Field
}
var a = new A();
a.y; // Accessing public field y: OK
a.#x; // Syntax error: reference to undeclared private field
甚至JavaScript给你提供的其他各种查询对象的工具也被阻止访问私有字段(例如,Object.getOwnProperty{Symbols,Names} 不列出私有字段;没有办法使用Reflect.get 来访问它们)。
一个特性的三种方式
当谈论JavaScript中的一个特性时,通常有三个不同的方面在起作用:心理模型、规范和实现。
心智模型提供了高层次的思维方式,我们希望程序员们大部分都能使用。而规范则提供了该功能所需的语义细节。只要能保持规范的语义,实现可以与规范文本有很大的不同。
这三个方面对于推理事物的人来说不应该产生不同的结果(不过,有时 "心理模型 "是速记的,并不能准确捕捉边缘情况下的语义)。
我们可以用这三个方面来看待私有领域。
心理模型
对于私有字段来说,最基本的心理模型就是它的字面意思:字段,但是私有。现在,JS字段变成了对象上的属性,所以心理模型可能是 "不能从类外访问的属性"。
然而,当我们遇到代理时,这种心理模型就有点崩溃了;试图指定 "隐藏属性 "和代理的语义是很有挑战性的(当代理试图提供对属性的访问控制时,如果你不应该用代理看到私有字段,会发生什么?子类可以访问私有字段吗?私有字段是否参与到原型继承中?)。为了保留所需的隐私属性,一个替代的心理模型成为委员会思考私有字段的方式。
这个替代模型被称为 "WeakMap "模型。在这个心理模型中,你想象每个类都有一个与每个私有字段相关的隐藏的弱映射,这样你就可以假设"解糖
class A {
#x = 15;
g() {
return this.#x;
}
}
变成这样的东西
class A_desugared {
static InaccessibleWeakMap_x = new WeakMap();
constructor() {
A_desugared.InaccessibleWeakMap_x.set(this, 15);
}
g() {
return A_desugared.InaccessibleWeakMap_x.get(this);
}
}
WeakMap ,令人惊讶的是,这个模型并不是规范中的功能写法,而是它们背后的设计意图的重要组成部分。我将在后面介绍一下这种心理模型是如何在一些地方出现的。
规范
实际的规范修改是由类域提案提供的,特别是对规范文本的修改。我不会涵盖这个规范文本的每一个部分,但我将指出具体的方面,以帮助阐明规范文本和实现之间的差异。
首先,该规范添加了一个概念 [[PrivateName]]的概念,它是一个全局唯一的字段标识符。这种全局唯一性是为了确保两个类不能仅仅因为名字相同而互相访问对方的字段。
function createClass() {
return class {
#x = 1;
static getX(o) {
return o.#x;
}
};
}
let [A, B] = [0, 1].map(createClass);
let a = new A();
let b = new B();
A.getX(a); // Allowed: Same class
A.getX(b); // Type Error, because different class.
该规范还添加了一个新的"内部槽",这是规范中与对象相关联的一个规范级的内部状态,称为 [[PrivateFieldValues]]到所有对象。[[PrivateFieldValues]] 是一个形式的记录列表。
{
[[PrivateName]]: Private Name,
[[PrivateFieldValue]]: ECMAScript value
}
为了操作这个列表,规范增加了四个新的算法。
这些算法在很大程度上像你所期望的那样工作:PrivateFieldAdd 向列表添加一个条目(不过,为了急于提供错误,如果列表中已经存在一个匹配的 Private Name,它将抛出一个TypeError 。我将在后面展示这种情况如何发生)。PrivateFieldGet 检索存储在列表中的一个值,以给定的 Private Name 为关键,等等。
构造函数重写技巧
当我第一次开始阅读规范时,我惊讶地发现PrivateFieldAdd 可以抛出。鉴于它只是从正在构建的对象上的构造函数中调用,我完全以为对象会是新创建的,因此你不需要担心一个字段已经存在了。
事实证明这是可能的,这是规范中对构造函数返回值的一些处理的副作用。为了更加具体,下面是André Bargull提供给我的一个例子,它显示了这一点的作用。
class Base {
constructor(o) {
return o; // Note: We are returning the argument!
}
}
class Stamper extends Base {
#x = "stamped";
static getX(o) {
return o.#x;
}
}
Stamper 是一个可以将其私有字段 "盖 "在任何对象上的类。
let obj = {};
new Stamper(obj); // obj now has private field #x
Stamper.getX(obj); // => "stamped"
这意味着,当我们向一个对象添加私有字段时,我们不能假设它没有这些字段。这就是PrivateFieldAdd 中的预存在检查发挥作用的地方。
let obj2 = {};
new Stamper(obj2);
new Stamper(obj2); // Throws 'TypeError' due to pre-existence of private field
这种将私有字段加到任意对象上的能力在这里也与WeakMap模型有一些互动。例如,考虑到你可以在任何对象上标示私有字段,这意味着你也可以在一个密封的对象上标示一个私有字段。
var obj3 = {};
Object.seal(obj3);
new Stamper(obj3);
Stamper.getX(obj3); // => "stamped"
如果你把私有字段想象成属性,这就不舒服了,因为这意味着你在修改一个被程序员封存的对象,以便将来修改。然而,使用弱映射模型,这是完全可以接受的,因为你只是把被密封的对象作为弱映射中的一个键。
PS:你可以把私有字段印到任意对象中,但并不意味着你应该这样做。请不要这样做。
实现规范
当面临实现规范时,在遵循规范的字面意思和做一些不同的事情以改善某些方面的实现之间存在着矛盾。
如果有可能直接实现规范的步骤,我们更愿意这样做,因为这样可以在规范发生变化时更容易维护功能。SpiderMonkey在很多地方都这样做。你会看到一些代码部分是对规范算法的转录,并有步骤 编号作为注释。在规范非常复杂的情况下,遵循规范的确切文字也是有帮助的,小的分歧可能导致兼容性风险。
然而,有时也有很好的理由与规范语言相背离。多年来,JavaScript的实现一直在为高性能而努力,并且有许多实现技巧被应用来实现这一目标。有时候,用已经写好的代码来重铸规范的一部分是正确的,因为这意味着新的代码也能够具有已经写好的代码的性能特征。
实现私有名称
私有名称的规范语言已经几乎与围绕着的语义相匹配了。 Symbols的语义,这在SpiderMonkey中已经存在。所以添加PrivateNames 作为Symbol 的一种特殊类型是一个相当容易的选择。
实现私有字段
看一下私有字段的规范,规范的实现是给SpiderMonkey中的每个对象添加一个额外的隐藏槽,其中包含对{PrivateName, Value} 对列表的引用。然而,直接实现这个有很多明显的缺点。
- 它给没有私有字段的对象增加了内存占用
- 它需要侵入性地增加新的字节码或对性能敏感的属性访问路径的复杂性。
另一个选择是偏离规范语言,只实现语义,不实现实际的规范算法。在大多数情况下,你真的可以把私有字段看作是对象上的特殊属性,这些属性在类之外被隐藏起来,不被反射或反省。
如果我们将私有字段建模为属性,而不是与对象一起维护的特殊边栏,我们就能够利用属性操作在JavaScript引擎中已经被极度优化这一事实。
然而,属性会受到反射的影响。因此,如果我们将私有字段建模为对象属性,我们需要确保反射API不会暴露它们,并且你不能通过代理来访问它们。
在SpiderMonkey中,我们选择将私有字段实现为隐藏的属性,以便利用引擎中已经存在的所有针对属性的优化机制。当我开始实现这个功能时,André Bargull--一个多年的SpiderMonkey贡献者--实际上给了我一系列的补丁,其中有很大一部分的私有字段实现已经完成,对此我非常感激。
使用我们特殊的PrivateName符号,我们有效地取消了
class A {
#x = 10;
x() {
return this.#x;
}
}
到一个看起来更接近于
class A_desugared {
constructor() {
this[PrivateSymbol(#x)] = 10;
}
x() {
return this[PrivateSymbol(#x)];
}
}
然而,私有字段的语义与属性略有不同。它们被设计为对预计是编程错误的模式发出错误,而不是默默地接受它。比如说。
- 在一个没有属性的对象上访问一个a属性,会返回
undefined。私有字段被指定为抛出一个TypeError,作为PrivateFieldGet算法的结果。 - 在一个没有属性的对象上设置一个属性,只是简单地添加该属性。私有字段会在
TypeErrorPrivateFieldSet. - 将一个私有字段添加到一个已经有该字段的对象上,也会抛出一个
TypeErrorinPrivateFieldAdd.关于这种情况是如何发生的,请看上面的 "构造函数覆盖技巧"。
为了处理不同的语义,我们修改了对私有字段访问的字节码排放。我们添加了一个新的字节码操作,CheckPrivateField ,它可以验证一个对象对于一个给定的私有字段具有正确的状态。这意味着如果该属性缺失或存在,就会抛出一个异常,这对于Get/Set或Add来说是合适的。CheckPrivateField 是在使用常规的 "计算属性名称 "路径(用于A[someKey] )之前发出的。
CheckPrivateField 在设计上,我们可以使用CacheIR轻松实现内联缓存。由于我们将私有字段存储为属性,我们可以使用对象的Shape作为防护,并简单地返回适当的布尔值。SpiderMonkey中对象的形状决定了它有哪些属性,以及它们在该对象的存储空间中的位置。具有相同形状的对象保证具有相同的属性,它是一个IC的完美检查,用于 。CheckPrivateField
我们对引擎所做的其他修改包括从属性枚举协议中省略了私有字段,如果我们要添加私有字段,则允许扩展密封对象。
代理人
代理人给我们带来了一些新的挑战。具体来说,使用上面的Stamper 类,你可以直接在代理中添加一个私有字段。
let obj3 = {};
let proxy = new Proxy(obj3, handler);
new Stamper(proxy)
Stamper.getX(proxy) // => "stamped"
Stamper.getX(obj3) // TypeError, private field is stamped
// onto the Proxy Not the target!
我最初肯定发现这很令人惊讶。我之所以觉得惊讶,是因为我本来以为,像其他操作一样,添加一个私有字段会通过代理隧道到达目标。然而,一旦我能够内化WeakMap的心理模型,我就能够更好地理解这个例子。诀窍在于,在WeakMap模型中,是Proxy ,而不是目标对象,作为#x WeakMap中的键。
然而,这些语义对我们选择将私有字段建模为隐藏属性的实现提出了挑战,因为SpiderMonkey的代理是高度专业化的对象,没有空间容纳任意的属性。为了支持这种情况,我们为 "expando "对象添加了一个新的保留槽。expansiono是一个懒惰地分配的对象,它充当代理上动态添加的属性的持有者。这种模式已经被用于DOM对象,它通常被实现为C++对象,没有多余的属性空间。因此,如果你写了document.foo = "hi" ,这就为document 分配了一个expando对象,并把foo 的属性和值放在里面。回到私有字段,当#x 在代理上被访问时,代理代码知道要去找expando对象中的那个属性。
综上所述
私有字段是实现JavaScript语言特性的一个例子,在这种情况下,直接实现所写的规范会比用已经优化的引擎基元来重铸规范的性能要低。然而,这种重铸本身就需要解决一些规范中没有的问题。
最后,我对我们为实现Private Fields所做的选择相当满意,并且很高兴看到它最终进入了这个世界。
鸣谢
我必须再次感谢André Bargull,他提供了第一套补丁,并为我铺设了一条很好的道路。他的工作使完成私人领域的工作变得更加容易,因为他已经在决策上花了很多心思。
在我完成这个实现的过程中,Jason Orendorff一直是一位优秀而耐心的导师,包括两个独立的私有字段字节码的实现,以及两个独立的代理支持的实现。
感谢Caroline Cullen和Iain Ireland帮助阅读这篇文章的草稿,并感谢Steve Fink修改了许多错别字。
The postImplementing Private Fields for JavaScriptappeared first onMozilla Hacks - the Web developer blog.