为JavaScript实现私有字段

2021-06-14 21:34:39

在实现JavaScript的语言功能时,实施者必须决定规范中的语言如何映射到实现。有时这相当简单,规范和实现可以分享大部分相同的术语和算法。其他时间,实施中的压力使得它更具挑战性,需要或迫使实施策略分歧与语言规范不同。

私有字段是规范语言和实现现实分歧的示例,至少在SpiderMoNkey- Powers Firefox的JavaScript引擎。要了解更多,我将解释一下私有字段,这是一个用于思考它们的模型,并解释为什么我们的实现从规范语言发出。

私有字段是通过TC39提议进程添加到JavaScript语言的语言功能,作为类字段的一部分,其在TC39过程中的阶段4。我们将在Firefox 90中运送私人字段和私人方法。

私有字段提议对语言的“私有状态”的严格概念增加了严格的概念。在以下示例中,#x只能由A类的实例访问:

这意味着在类之外,无法访问该字段。例如,与公共字段不同,如以下示例显示:

A类{#X = 10; //私人字段Y = 12; //公共字段} var a = new a(); a.y; //访问公共字段Y:OKA。#x; //语法错误:引用undeclared私有字段

甚至可以阻止JavaScript为您提供询问对象的各种其他工具访问私人字段(例如,object.getownProperty {符号,名称}不列出私人字段;没有办法使用reflect.get访问它们)。

在谈论JavaScript中的功能时,播放中通常有三个不同的方面:心理模型,规范和实现。

心理模型提供了高级思维,我们希望程序员主要使用。该规范依次提供特征所需的语义的详细信息。只要维持规范语义即可,实现就会看起来与规范文本不同。

这三个方面不应该对人们推理的不同结果(尽管,有时,“心理模型”是简写的,并且在边缘案例场景中没有准确地捕获语义)。

最基本的心理模型可以为私人领域拥有,是它在TIN:字段中所说的,但私有。现在,JS字段成为对象的属性,因此心理模型可能是无法从类外部访问的属性。

但是,当我们遇到代理时,这种心理模型会稍微崩溃;尝试指定“隐藏属性”和代理的语义是具有挑战性的(当您尝试为属性提供访问控制时会发生什么,如果您不应该看到具有代理的私人字段?可以子类访问私人字段吗?私人字段是否参与原型继承?)。为了保留所需的隐私权,替代的精神模型成为委员会对私人领域的看法。

该替代模型称为“弱势地图”模型。在这个心理模型中,你想象每个班级都有一个隐藏的弱点地图与每个私人领域相关联,这样你就可以假设'desugar'

class a_desugared {static inaccessibleweakmap_x = new devemap();构造函数(){a_desugared.inaccessibleweakmap_x.set(此,15); } g(){return a_desugared.inaccessibleweakmap_x.get(这); }}

令人惊讶的是,弱势模型不是如何在规范中编写的功能,而是设计意图的重要组成部分。我会稍后介绍这个心理模型在后面的地方出现。

实际规范更改由类字段提案提供,特别是调整文本的更改。我不会涵盖每一块规范文本,但我会呼吁特定方面来帮助阐明规范文本和实施之间的差异。

首先,规范添加了[[privateName]]的概念,它是全局唯一的字段标识符。这种全局唯一性是确保通过具有相同名称,确保两个类无法仅访问彼此的字段。

function createClass(){return类{#x = 1;静态getx(o){返回o。#x; }};} Let [A,B] = [0,1] .map(CreateClass);让= new a();设b = new b(); a.getx(a); //允许:相同的classa.getx(b); //类型错误,因为不同的类。

该规范还添加了一个新的“内插槽”,它是与SPEG中的对象相关联的规范级别的内部状态,称为[privateFieldValues]]到所有对象。 [[PrivateFieldValues]]是表单的记录列表:

这些算法在很大程度上工作,就像你期望的那样:privateFieldAdd将一个条目附加到列表(尽管如此,如果在列表中存在匹配的私有名称,则尝试急切地提供错误,但它将抛出TypeError。我会展示那可以发生如何发生)。 PrivateFieldGet检索存储在列表中的值,由给定的私有名称等键入。

当我第一次开始阅读规范时,我惊讶地看到PrivateFieldAdd可以抛出。鉴于它仅从构造的对象上的构造函数调用,我完全预计该对象将是新创建的,因此您不需要担心已经存在的领域。

这结果是可能的,一些规范的构造函数返回值的副作用。更具体地,以下是AndréBargull提供给我的一个例子,它在行动中表明了这一点。

类基础{构造函数(o){return o; //注意:我们正在返回争论! }}类压模延伸基础{#x ="盖章&#34 ;;静态getx(o){返回o。#x; }}

Stamper是一个可以在任何对象上'邮戳'私有字段的类:

让obj = {};新的压模(obj); // obj现在有私有字段#xstamper.getx(obj); // => "盖章"

这意味着当我们向一个对象添加私人字段时,我们无法假设它没有它们已经存在。这是PrivateFieldAdd中存在的预存检查中的位置:

让obj2 = {};新的压模(obj2);新的压模(obj2); //抛出' typeerror'由于私人领域的预先存在

这种将私有字段列入任意物体的能力与此处的弱势模型相互作用。例如,鉴于您可以将私有字段标记到任何对象上,这意味着您也可以将私有字段标记到密封对象上:

如果将私有字段视为属性,则这是不舒服的,因为它意味着您可以修改由程序员密封的对象,以便将来修改密封。但是,使用弱图模型,它是完全可以接受的,因为您只使用密封对象作为弱图中的键。

PS:只是因为你可以将私有字段映射到任意对象中,并不意味着你应该:请不要这样做。

当面对实施规范时,在规范的字母之间存在紧张,并在某些维度上做出不同的事情以改善实现。

在可以直接实现规范的步骤的情况下,我们更愿意这样做,因为它使特征的维护更容易,因为规范变化更改。 Spidermonkey在很多地方做到这一点。您将看到代码的代码部分,这些代码是规范算法的转录,具有评论的步骤编号。在规范的确切字母之后,在规范高度复杂并且小的分歧可能导致兼容性风险的情况下,也有助于。

然而,有时,有很好的理由从规范语言中分歧。 JavaScript实现已经磨练了几年的高性能,并且有许多实施技巧已应用于发生这种情况。有时在已经写入的代码中重新定位规范的一部分是正确的事情,因为这意味着新代码也能够具有已经写入代码的性能特征。

私有名称的规范语言已经与符号周围的语义相匹配,这些语义已经存在于spidermonkey中。因此,将私有名添加为特殊类型的符号是一个相当简单的选择。

查看私有字段的规范,规范实现将是向SpiderMoNkey中的每个对象添加额外的隐藏插槽,该对象包含对{privateName,value}对的列表的引用。但是,实现这一点直接有许多清晰的缺点:

它需要侵入性添加新的字节码或复杂性对性能敏感的属性访问路径。

另一种选择是从规范语言分歧,仅实现语义,而不是实际规范算法。在大多数情况下,您真的可以将私有字段视为隐藏在课外反射或内省的物体上的特殊属性。

如果我们将私有字段塑造为属性,而不是用对象维护的特殊侧面列表,我们能够利用属性操作在JavaScript引擎中非常优化。

但是,属性受到反射。因此,如果将私有字段模拟为对象属性,我们需要确保反射API不会显示它们,并且您无法通过代理访问它们。

在Spidermonkey中,我们选择将私有字段实施为隐藏属性,以便利用引擎中已存在的所有优化机器。当我开始实施这个功能AndréBargull时 - 蜘蛛侠贡献者多年来 - 实际上递给了一系列有很好的私人领域实现的补丁,所以已经完成了,我非常感激。

然而,私有字段的语义略有不同。它们旨在为预期编程错误的模式发出错误,而不是默默地接受它。例如:

访问一个属性的对象中没有返回的对象未定义。由于PrivateFieldGet算法,指定私有字段以抛出TypeError。

在没有具有它的对象上设置属性只是添加属性。私有字段将在PriveyFieldSet中抛出TypeError。

将私有字段添加到已有该字段的对象也将在PriveyFieldAdd中抛出TypeError。有关如何发生这种情况,请参阅上面的“构造函数覆盖技巧”。

要处理不同的语义,我们修改了私人实地访问的字节码发射。我们添加了一个新的字节码op,checkprivatefield,验证对象具有给定私有字段的正确状态。这意味着如果属性丢失或呈现,则抛出异常,适用于获取/设置或添加。在使用常规“计算属性名称”路径之前(用于[某些键])的路径,请刚刚发出CheckPrivateField。

CheckPrivateField是设计,使我们可以使用CacheIR轻松实现内联高速缓存。由于我们将私有字段存储为属性,因此我们可以使用对象的形状作为警卫,并且只需返回适当的布尔值。 spidermonkey中的对象的形状确定它具有哪些属性,并且它们位于该对象的存储器中。保证具有相同形状的对象具有相同的属性,并且对CheckPrivateField的IC完美检查。

我们对引擎进行的其他修改包括从属性枚举协议中省略私有字段,并允许如果我们添加私人字段,则允许密封对象的扩展。

代理给我们呈现了一些新挑战。具体地,使用上面的压模类,您可以直接添加私人字段到代理:

让obj3 = {};留言=新代理(obj3,处理程序);新的压模(代理)stamper.getx(代理)// => " Stamped" stamper.getx(obj3)// typearror,私有字段被冲压//在代理上而不是目标!

我绝对发现了这个令人惊讶的最初。我发现这个令人惊讶的原因是我预期,就像其他操作一样,私有字段的添加将通过代理到目标隧道。但是,一旦我能够内化弱势地区精神模型,我就能更好地理解这个例子。诀窍在于,在弱势模型中,它是代理,而不是目标对象,用作#x弱模型中的键。

这些语义向我们的实施选择提出了一个挑战,以将私有字段模拟为隐藏属性,因为SpiderMoNkey的代理是高度专业化的对象,这些对象没有任意属性的空间。为了支持这种情况,我们为“Expando”对象添加了一个新的保留插槽。扩展是懒惰地分配的对象,其充当持有者,用于在代理上动态地添加属性。此模式已用于DOM对象,通常使用++对象,没有额外属性的空间。所以,如果你写文档.Foo ="嗨",这为文档分配了一个拓拓对象,并在那里施加foo属性和值。返回私有字段,当代理上访问#x时,代理代码知道要去查看该属性的Expando对象。

私有字段是实现JavaScript语言功能的实例,在那里直接实现所写的规范,而不是在已经优化的发动机基元方面重新施加规范。然而,重铸本身可能需要一些问题不存在于规范中。

最后,我非常满意对我们实施私人领域的选择,我很高兴看到它最终进入世界!

我要再次感谢,andréBargull,谁提供了第一套补丁并为我奠定了一个优秀的小道。他的工作使私人田地更加容易,因为他已经思考了决策。

jason orendorff是一位优秀的患者的导师,因为我通过这个实现工作,包括私有字段字节码的两个单独实现,以及两个单独的代理支持实现。

感谢Caroline Cullen,以及Iain爱尔兰帮助阅读这篇文章的草案,并向史蒂夫·芬克修复许多拼写错误。