get
,set
)get
,set
)get
)set
)enumerate
陷阱在哪里?代理使您能够拦截和自定义对对象执行的操作(例如获取属性)。它们是一种*元编程*特性。
在以下示例中,proxy
是我们要拦截其操作的对象,而 handler
是处理拦截的对象。在这种情况下,我们只拦截一个操作,即 get
(获取属性)。
当我们获取属性 proxy.foo
时,处理器会拦截该操作
有关可以拦截的操作列表,请参阅完整 API 参考。
在我们深入了解代理是什么以及它们为什么有用之前,我们首先需要了解什么是*元编程*。
在编程中,存在不同的级别
基础级别和元级别可以使用不同的语言。在以下元程序中,元编程语言是 JavaScript,而基础编程语言是 Java。
元编程可以采取不同的形式。在上一个示例中,我们将 Java 代码打印到控制台。让我们同时使用 JavaScript 作为元编程语言和基础编程语言。这方面的经典示例是eval()
函数,它允许您动态地评估/编译 JavaScript 代码。eval()
的实际用例并不多。在下面的交互中,我们使用它来评估表达式 5 + 2
。
其他 JavaScript 操作可能看起来不像元编程,但如果仔细观察,它们实际上是
程序在运行时检查自身的结构。这看起来不像元编程,因为在 JavaScript 中,编程结构和数据结构之间的界限很模糊。所有Object.*
方法都可以被视为元编程功能。
反射式元编程意味着程序处理自身。Kiczales 等人 [2] 区分了三种反射式元编程
让我们看一些例子。
**示例:内省。** Object.keys()
执行内省(参见上一个示例)。
**示例:自我修改。** 以下函数 moveProperty
将属性从源移动到目标。它通过用于属性访问的方括号运算符、赋值运算符和 delete
运算符执行自我修改。(在生产代码中,您可能使用属性描述符来完成此任务。)
使用 moveProperty()
ECMAScript 5 不支持拦截;创建代理是为了填补这一空白。
ECMAScript 6 代理为 JavaScript 带来了拦截。它们的工作原理如下。您可以对对象 obj
执行许多操作。例如
obj
的属性 prop
(obj.prop
)obj
是否具有属性 prop
('prop' in obj
)代理是允许您自定义其中一些操作的特殊对象。代理使用两个参数创建
handler
:对于每个操作,都有一个相应的处理器方法,如果存在,则执行该操作。这种方法*拦截*操作(在到达目标的途中),并被称为*陷阱*(从操作系统领域借用的术语)。target
:如果处理器没有拦截操作,则在目标上执行该操作。也就是说,它充当处理器的后备。在某种程度上,代理包装了目标。在以下示例中,处理器拦截了 get
和 has
操作。
当我们获取属性 foo
时,处理器会拦截该操作
同样,in
运算符会触发 has
处理器没有实现陷阱 set
(设置属性)。因此,设置 proxy.bar
会转发到 target
,并导致设置 target.bar
。
如果目标是函数,则可以拦截另外两个操作
apply
:进行函数调用,通过以下方式触发proxy(···)
proxy.call(···)
proxy.apply(···)
construct
:进行构造函数调用,通过以下方式触发new proxy(···)
仅对函数目标启用这些陷阱的原因很简单:否则,您将无法转发 apply
和 construct
操作。
如果要通过代理拦截方法调用,则存在一个挑战:您可以拦截操作 get
(获取属性值),也可以拦截操作 apply
(调用函数),但是没有针对可以拦截的方法调用的单个操作。这是因为方法调用被视为两个独立的操作:首先是 get
来检索函数,然后是 apply
来调用该函数。
因此,您必须拦截 get
并返回一个拦截函数调用的函数。以下代码演示了如何完成此操作。
我没有为后一项任务使用代理,我只是用一个函数包装了原始方法。
让我们使用以下对象来试用 traceMethodCalls()
tracedObj
是 obj
的跟踪版本。每次方法调用后的第一行是 console.log()
的输出,第二行是方法调用的结果。
好消息是,即使在 obj.squared()
内部进行的调用 this.multiply()
也会被跟踪。这是因为 this
继续引用代理。
这不是最有效的解决方案。例如,可以缓存方法。此外,代理本身也会影响性能。
ECMAScript 6 允许您创建可以*撤销*(关闭)的代理
在赋值运算符 (=
) 的左侧,我们使用解构来访问 Proxy.revocable()
返回的对象的属性 proxy
和 revoke
。
首次调用函数 revoke
后,您对 proxy
应用的任何操作都会导致 TypeError
。后续调用 revoke
不会产生任何影响。
代理 proto
可以成为对象 obj
的原型。某些在 obj
中开始的操作可能会在 proto
中继续。其中一个操作是 get
。
在 obj
中找不到属性 bla
,这就是为什么搜索在 proto
中继续并在那里触发陷阱 get
的原因。还有更多影响原型的操作;它们列在本章末尾。
处理器没有实现其陷阱的操作会自动转发到目标。有时,除了转发操作之外,您还想执行一些任务。例如,一个拦截所有操作并记录它们的处理器,但不会阻止它们到达目标
对于每个陷阱,我们首先记录操作的名称,然后通过手动执行操作来转发它。ECMAScript 6 具有类似模块的对象 Reflect
,它有助于转发:对于每个陷阱
Reflect
都有一个方法
如果我们使用 Reflect
,则前面的示例如下所示。
现在,每个陷阱的作用都非常相似,我们可以通过代理来实现处理器
对于每个陷阱,代理都通过 get
操作请求一个处理器方法,我们给它一个。也就是说,所有处理器方法都可以通过单个元方法 get
来实现。使这种虚拟化变得简单是代理 API 的目标之一。
让我们使用这个基于代理的处理器
以下交互确认 set
操作已正确转发到目标
代理对象可以看作是拦截对其目标对象执行的操作——代理包装了目标。代理的处理器对象就像代理的观察者或侦听器。它通过实现相应的方法(get
用于读取属性等)来指定应拦截哪些操作。如果缺少某个操作的处理器方法,则不会拦截该操作。它只是被转发到目标。
因此,如果处理器是空对象,则代理应该透明地包装目标。唉,这并不总是有效。
this
在我们深入探讨之前,让我们快速回顾一下包装目标如何影响 this
如果您直接调用 target.foo()
,则 this
指向 target
如果您通过代理调用该方法,则 this
指向 proxy
这样做是为了让代理在例如目标调用 this
上的方法时继续处于循环中。
通常情况下,带有空处理程序的代理可以透明地包装目标:您不会注意到它们的存在,并且它们不会改变目标的行为。
但是,如果目标通过代理无法控制的机制将信息与 this
关联,则会出现问题:事情会失败,因为关联的信息取决于目标是否被包装。
例如,以下类 Person
将私有信息存储在 WeakMap _name
中(有关此技术的更多信息,请参阅关于类的章节)
Person
的实例无法透明包装
jane.name
与包装后的 proxy.name
不同。以下实现没有此问题
大多数内置构造函数的实例也具有代理无法拦截的机制。因此,它们也不能透明地包装。我将演示 Date
实例的问题
不受代理影响的机制称为*内部插槽*。这些插槽是与实例关联的类似属性的存储。规范将这些插槽视为名称在方括号中的属性。例如,以下方法是内部方法,可以在所有对象 O
上调用
但是,对内部插槽的访问不是通过普通的“获取”和“设置”操作进行的。如果通过代理调用 getDate()
,它将无法在 this
上找到所需的内部插槽,并通过 TypeError
报错。
对于 Date
方法,语言规范指出
除非另有明确说明,否则下面定义的 Number 原型对象的方法不是通用的,并且传递给它们的值必须是 Number 值或具有已初始化为 Number 值的
[[NumberData]]
内部插槽的对象。
与其他内置函数相比,数组可以透明包装
数组可包装的原因是,即使属性访问已自定义以使 length
工作,但数组方法也不依赖于内部插槽 - 它们是通用的。
作为解决方法,您可以更改处理程序转发方法调用的方式,并有选择地将 this
设置为目标而不是代理
这种方法的缺点是该方法在 this
上执行的操作都不会通过代理。
**致谢:**感谢 Allen Wirfs-Brock 指出本节中解释的陷阱。
本节演示代理的用途。这将使您有机会看到 API 的实际应用。
get
、set
) 假设我们有一个函数 tracePropAccess(obj, propKeys)
,每当设置或获取 obj
的属性(其键位于数组 propKeys
中)时,该函数都会记录日志。在以下代码中,我们将该函数应用于类 Point
的实例
获取和设置被跟踪对象 p
的属性具有以下效果
有趣的是,每当 Point
访问属性时,跟踪也会起作用,因为 this
现在指的是被跟踪的对象,而不是 Point
的实例。
在 ECMAScript 5 中,您将按如下方式实现 tracePropAccess()
。我们将每个属性替换为一个 getter 和一个 setter,用于跟踪访问。setter 和 getter 使用一个额外的对象 propData
来存储属性的数据。请注意,我们正在破坏性地更改原始实现,这意味着我们正在进行元编程。
在 ECMAScript 6 中,我们可以使用更简单的、基于代理的解决方案。我们拦截属性获取和设置,而不必更改实现。
get
、set
) 在访问属性时,JavaScript 非常宽容。例如,如果您尝试读取属性并拼写错误其名称,则不会收到异常,而是收到结果 undefined
。您可以使用代理在这种情况下获取异常。其工作原理如下。我们将代理设为对象的原型。
如果在对象中找不到属性,则会触发代理的 get
陷阱。如果在代理之后的原型链中甚至不存在该属性,则该属性确实丢失,我们会抛出异常。否则,我们将返回继承属性的值。我们通过将 get
操作转发到目标来做到这一点(目标的原型也是代理的原型)。
让我们将 PropertyChecker
用于我们创建的对象
如果我们将 PropertyChecker
转换为构造函数,我们可以通过 extends
将其用于 ECMAScript 6 类
如果您担心意外*创建*属性,则有两种选择:您可以包装一个代理来捕获 set
的对象。或者,您可以通过 Object.preventExtensions(obj)
使对象 obj
不可扩展,这意味着 JavaScript 不允许您向 obj
添加新的(自身)属性。
get
) 一些数组方法允许您通过 -1
引用最后一个元素,通过 -2
引用倒数第二个元素,等等。例如
唉,这在通过括号运算符 ([]
) 访问元素时不起作用。但是,我们可以使用代理来添加该功能。以下函数 createArray()
创建支持负索引的数组。它通过在数组实例周围包装代理来实现。代理拦截由括号运算符触发的 get
操作。
致谢:此示例的想法来自 hemanth.hm 的博客文章。
set
) 数据绑定是在对象之间同步数据。一种流行的用例是基于 MVC(模型-视图-控制器)模式的小部件:使用数据绑定,如果更改*模型*(由小部件可视化的数据),则*视图*(小部件)将保持最新。
要实现数据绑定,您必须观察并响应对对象所做的更改。在以下代码片段中,我概述了如何观察数组的更改。
输出
代理可用于创建可以调用任意方法的对象。在以下示例中,函数 createWebService
创建了一个这样的对象 service
。在 service
上调用方法将检索具有相同名称的 Web 服务资源的内容。检索是通过 ECMAScript 6 Promise 处理的。
以下代码是 ECMAScript 5 中 createWebService
的快速而粗糙的实现。因为我们没有代理,所以我们需要事先知道将在 service
上调用哪些方法。参数 propKeys
为我们提供了该信息,它包含一个包含方法名称的数组。
ECMAScript 6 中 createWebService
的实现可以使用代理,并且更简单
两种实现都使用以下函数发出 HTTP GET 请求(其工作原理在关于 Promise 的章节中进行了说明)。
*可撤销引用*的工作原理如下:不允许客户端直接访问重要资源(对象),只能通过引用(中间对象,资源的包装器)访问。通常,应用于引用的每个操作都会转发到资源。客户端完成后,通过*撤销*引用(关闭引用)来保护资源。此后,对引用应用操作会引发异常,并且不再转发任何内容。
在以下示例中,我们为资源创建了一个可撤销的引用。然后,我们通过引用读取资源的属性之一。这是可行的,因为引用授予我们访问权限。接下来,我们撤销引用。现在,引用不再让我们读取属性了。
代理非常适合实现可撤销引用,因为它们可以拦截和转发操作。这是 createRevocableReference
的简单基于代理的实现
可以使用上一节中的代理作为处理程序技术来简化代码。这一次,处理程序基本上是 Reflect
对象。因此,get
陷阱通常返回适当的 Reflect
方法。如果引用已被撤销,则会抛出 TypeError
。
但是,您不必自己实现可撤销引用,因为 ECMAScript 6 允许您创建可以撤销的代理。这一次,撤销发生在代理中,而不是在处理程序中。处理程序要做的就是将每个操作转发到目标。正如我们所见,如果处理程序没有实现任何陷阱,这会自动发生。
*膜*建立在可撤销引用的基础上:设计用于运行不受信任代码的环境会在该代码周围包裹一层膜,以隔离代码并确保系统其余部分的安全。对象在两个方向上传递膜
在这两种情况下,可撤销引用都包装在对象周围。包装函数或方法返回的对象也被包装。此外,如果将包装的湿对象传递回膜中,则会将其解包。
不受信任的代码完成后,所有可撤销的引用都将被撤销。结果,它在外部的任何代码都无法再执行,并且它拥有的外部对象也将停止工作。Caja 编译器是“一种用于使第三方 HTML、CSS 和 JavaScript 安全地嵌入到您的网站中的工具”。它使用膜来完成这项任务。
浏览器文档对象模型 (DOM) 通常作为 JavaScript 和 C++ 的混合实现。在纯 JavaScript 中实现它对于以下方面很有用
唉,标准 DOM 可以做一些在 JavaScript 中不容易复制的事情。例如,大多数 DOM 集合都是 DOM 当前状态的实时视图,每当 DOM 发生更改时,这些视图都会动态更改。结果,DOM 的纯 JavaScript 实现效率不高。将代理添加到 JavaScript 的原因之一是帮助编写更高效的 DOM 实现。
代理还有更多用例。例如
在本节中,我们将深入探讨代理的工作原理以及它们为何以这种方式工作。
Firefox 允许您进行一些拦截式的元编程:如果您定义了一个名为 __noSuchMethod__
的方法,则每当调用不存在的方法时,它都会收到通知。以下是使用 __noSuchMethod__
的示例。
因此,__noSuchMethod__
的工作方式类似于代理陷阱。与代理不同,陷阱是我们想要拦截其操作的对象的自有方法或继承方法。这种方法的问题在于基础级别(普通方法)和元级别(__noSuchMethod__
)是混合的。基础级别代码可能会意外调用或看到元级别方法,并且有可能意外定义元级别方法。
即使在标准 ECMAScript 5 中,基础级别和元级别有时也会混合。例如,以下元编程机制可能会失败,因为它们存在于基础级别
obj.hasOwnProperty(propKey)
:如果原型链中的属性覆盖了内置实现,则此调用可能会失败。例如,如果 obj
是
调用此方法的安全方法是
func.call(···)
、func.apply(···)
:对于这两种方法,问题和解决方案与 hasOwnProperty
相同。obj.__proto__
:在大多数 JavaScript 引擎中,__proto__
是一个特殊属性,允许您获取和设置 obj
的原型。因此,当您将对象用作字典时,必须小心 避免将 __proto__
用作属性键。到目前为止,应该很明显,使(基础级别)属性键特殊是有问题的。因此,代理是*分层的*——基础级别(代理对象)和元级别(处理程序对象)是分开的。
代理在两个角色中使用
代理 API 的早期设计将代理视为纯粹的虚拟对象。但是,事实证明,即使在该角色中,目标也是有用的,可以强制执行不变量(稍后解释)并作为处理程序未实现的陷阱的回退。
代理以两种方式屏蔽
这两个原则都赋予代理模拟其他对象的强大能力。强制执行*不变量*(稍后解释)的一个原因是控制这种能力。
如果您确实需要一种方法来区分代理和非代理,则必须自己实现。以下代码是一个模块 lib.js
,它导出两个函数:一个函数创建代理,另一个函数确定对象是否是这些代理之一。
此模块使用 ECMAScript 6 数据结构 WeakSet
来跟踪代理。WeakSet
非常适合此目的,因为它不会阻止其元素被垃圾回收。
下一个示例显示了如何使用 lib.js
。
本节将检查 JavaScript 的内部结构以及如何选择代理陷阱集。
在编程语言和 API 设计的上下文中,*协议* 是一组接口以及使用它们的规则。ECMAScript 规范描述了如何执行 JavaScript 代码。它包括一个 用于处理对象的协议。此协议在元级别运行,有时称为元对象协议 (MOP)。JavaScript MOP 由所有对象都具有的自己的内部方法组成。“内部”意味着它们仅存在于规范中(JavaScript 引擎可能拥有也可能没有它们)并且无法从 JavaScript 访问。内部方法的名称用双括号括起来。
获取属性的内部方法称为 [[Get]]
。如果我们假设带有方括号的属性名称是合法的,那么此方法在 JavaScript 中将大致按如下方式实现。
此代码中调用的 MOP 方法是
[[GetOwnProperty]]
(陷阱 getOwnPropertyDescriptor
)[[GetPrototypeOf]]
(陷阱 getPrototypeOf
)[[Get]]
(陷阱 get
)[[Call]]
(陷阱 apply
)在 A 行中,您可以看到为什么原型链中的代理在“早期”对象中找不到属性时会找出 get
:如果没有键为 propKey
的自有属性,则搜索将继续在 this
的原型 parent
中进行。
**基本操作与派生操作。**您可以看到 [[Get]]
调用了其他 MOP 操作。执行此操作的操作称为*派生的*。不依赖于其他操作的操作称为*基本的*。
代理的元对象协议 与普通对象的元对象协议不同。对于普通对象,派生操作调用其他操作。对于代理,每个操作(无论它是基本的还是派生的)要么被处理程序方法拦截,要么转发到目标。
哪些操作应该可以通过代理进行拦截?一种可能性是仅为基本操作提供陷阱。另一种方法是包括一些派生操作。这样做的好处是可以提高性能并且更加方便。例如,如果没有 get
的陷阱,则必须通过 getOwnPropertyDescriptor
实现其功能。派生陷阱的一个问题是它们可能导致代理行为不一致。例如,get
返回的值可能与 getOwnPropertyDescriptor
返回的描述符中的值不同。
代理的拦截是*选择性的*:您不能拦截所有语言操作。为什么排除了一些操作?让我们看看两个原因。
首先,稳定的操作不适合拦截。如果一个操作对于相同的参数总是产生相同的结果,那么它就是*稳定的*。如果代理可以捕获稳定的操作,它就会变得不稳定,从而变得不可靠。严格相等 (===
) 就是这样一种稳定的操作。它不能被捕获,并且它的结果是通过将代理本身视为另一个对象来计算的。保持稳定性的另一种方法是对目标而不是代理应用操作。如后文所述,当我们查看如何对代理强制执行不变量时,当 Object.getPrototypeOf()
应用于目标不可扩展的代理时,就会发生这种情况。
不使更多操作可拦截的第二个原因是,拦截意味着在通常不可能的情况下执行自定义代码。代码的这种交织越多,程序就越难理解和调试。它还会对性能产生负面影响。
get
与 invoke
如果要通过 ECMAScript 6 代理创建虚拟方法,则必须从 get
陷阱返回函数。这就提出了一个问题:为什么不为方法调用引入额外的陷阱(例如 invoke
)?这将使我们能够区分
obj.prop
获取属性(陷阱 get
)obj.prop()
调用方法(陷阱 invoke
)不这样做有两个原因。
首先,并非所有实现都区分 get
和 invoke
。例如,Apple 的 JavaScriptCore 就没有。
其次,提取方法并稍后通过 call()
或 apply()
调用它应该与通过调度调用方法具有相同的效果。换句话说,以下两种变体应该等效地工作。如果有一个额外的陷阱 invoke
,那么这种等价性将更难维护。
invoke
的用例 有些事情只有在您能够区分 get
和 invoke
时才能完成。因此,使用当前的代理 API 不可能做到这些事情。两个例子是:自动绑定和拦截丢失的方法。让我们看看如果代理支持 invoke
,将如何实现它们。
**自动绑定。**通过使代理成为对象 obj
的原型,您可以自动绑定方法
obj.m
检索方法 m
的值将返回一个函数,其 this
绑定到 obj
。obj.m()
执行方法调用。自动绑定有助于将方法用作回调。例如,上一个示例中的变体 2 变得更简单
**拦截丢失的方法。**invoke
允许代理模拟前面提到的 Firefox 支持的 __noSuchMethod__
机制。代理将再次成为对象 obj
的原型。它会根据如何访问未知属性 foo
而做出不同的反应
obj.foo
读取该属性,则不会发生拦截,并且返回 undefined
。obj.foo()
,则代理会拦截并例如通知回调。在我们查看不变量是什么以及如何对代理强制执行它们之前,让我们回顾一下如何通过不可扩展性和不可配置性来保护对象。
有两种保护对象的方法
**不可扩展性。**如果对象是不可扩展的,则不能添加属性,也不能更改其原型
**不可配置性。**属性的所有数据都存储在*属性*中。属性就像一条记录,而属性就像该记录的字段。属性示例
value
保存属性的值。writable
控制是否可以更改属性的值。configurable
控制是否可以更改属性的属性。因此,如果一个属性既不可写也不可配置,那么它是只读的,并且保持这种状态
有关这些主题的更多详细信息(包括 Object.defineProperty()
的工作原理),请参阅“Speaking JavaScript”中的以下部分
传统上,不可扩展性和不可配置性是
这些和其他在面对语言操作时保持不变的特性称为*不变量*。使用代理,很容易违反不变量,因为它们本质上不受不可扩展性等的约束。
代理 API 通过检查处理程序方法的参数和结果来防止代理违反不变式。以下是四个不变式示例(针对任意对象 obj
)以及如何对代理强制执行它们(本章末尾提供了完整列表)。
前两个不变式涉及不可扩展性和不可配置性。这些是通过使用目标对象进行簿记来强制执行的:处理程序方法返回的结果必须与目标对象基本同步。
Object.preventExtensions(obj)
返回 true
,则所有未来的调用都必须返回 false
,并且 obj
现在必须是不可扩展的。true
但目标对象不可扩展,则通过抛出 TypeError
来对代理强制执行。Object.isExtensible(obj)
必须始终返回 false
。Object.isExtensible(target)
(强制转换后)不同,则通过抛出 TypeError
来对代理强制执行。其余两个不变式通过检查返回值来强制执行
Object.isExtensible(obj)
必须返回一个布尔值。Object.getOwnPropertyDescriptor(obj, ···)
必须返回一个对象或 undefined
。TypeError
来对代理强制执行。强制执行不变式具有以下好处
接下来的两节将举例说明如何强制执行不变式。
为了响应 getPrototypeOf
陷阱,如果目标不可扩展,则代理必须返回目标的原型。
为了演示此不变式,让我们创建一个处理程序,该处理程序返回与目标原型不同的原型
如果目标是可扩展的,则伪造原型有效
但是,如果我们为不可扩展的对象伪造原型,则会收到错误。
如果目标具有不可写不可配置的属性,则处理程序必须在响应 get
陷阱时返回该属性的值。为了演示此不变式,让我们创建一个处理程序,该处理程序始终为属性返回相同的值。
属性 target.foo
不是不可写且不可配置的,这意味着允许处理程序假装它具有不同的值
但是,属性 target.bar
是不可写且不可配置的。因此,我们无法伪造它的值
enumerate
陷阱在哪里? ES6 最初有一个由 for-in
循环触发的陷阱 enumerate
。但它最近被删除了,以简化代理。Reflect.enumerate()
也被删除了。(来源:TC39 笔记)
本节简要介绍了代理 API:全局对象 Proxy
和 Reflect
。
有两种方法可以创建代理
const proxy = new Proxy(target, handler)
const {proxy, revoke} = Proxy.revocable(target, handler)
revoke
撤销的代理。revoke
可以被调用多次,但只有第一次调用有效,并且会关闭 proxy
。之后,对 proxy
执行的任何操作都会导致抛出 TypeError
。本小节解释了处理程序可以实现哪些陷阱以及哪些操作会触发它们。一些陷阱会返回布尔值。对于陷阱 has
和 isExtensible
,布尔值是操作的结果。对于所有其他陷阱,布尔值指示操作是否成功。
所有对象的陷阱
defineProperty(target, propKey, propDesc) : boolean
Object.defineProperty(proxy, propKey, propDesc)
deleteProperty(target, propKey) : boolean
delete proxy[propKey]
delete proxy.foo // propKey = 'foo'
get(target, propKey, receiver) : any
receiver[propKey]
receiver.foo // propKey = 'foo'
getOwnPropertyDescriptor(target, propKey) : PropDesc|Undefined
Object.getOwnPropertyDescriptor(proxy, propKey)
getPrototypeOf(target) : Object|Null
Object.getPrototypeOf(proxy)
has(target, propKey) : boolean
propKey in proxy
isExtensible(target) : boolean
Object.isExtensible(proxy)
ownKeys(target) : Array<PropertyKey>
Object.getOwnPropertyPropertyNames(proxy)
(仅使用字符串键)Object.getOwnPropertyPropertySymbols(proxy)
(仅使用符号键)Object.keys(proxy)
(仅使用可枚举的字符串键;可枚举性通过 Object.getOwnPropertyDescriptor
检查)preventExtensions(target) : boolean
Object.preventExtensions(proxy)
set(target, propKey, value, receiver) : boolean
receiver[propKey] = value
receiver.foo = value // propKey = 'foo'
setPrototypeOf(target, proto) : boolean
Object.setPrototypeOf(proxy, proto)
函数的陷阱(如果目标是函数,则可用)
apply(target, thisArgument, argumentsList) : any
proxy.apply(thisArgument, argumentsList)
proxy.call(thisArgument, ...argumentsList)
proxy(...argumentsList)
construct(target, argumentsList, newTarget) : Object
new proxy(..argumentsList)
以下操作是*基本的*,它们不使用其他操作来完成它们的工作:apply
、defineProperty
、deleteProperty
、getOwnPropertyDescriptor
、getPrototypeOf
、isExtensible
、ownKeys
、preventExtensions
、setPrototypeOf
所有其他操作都是*派生的*,它们可以通过基本操作来实现。例如,对于数据属性,可以通过 getPrototypeOf
迭代原型链并为每个链成员调用 getOwnPropertyDescriptor
来实现 get
,直到找到自己的属性或链结束。
不变式是处理程序的安全约束。本小节记录了代理 API 强制执行的不变式以及如何强制执行。每当您在下面阅读“处理程序必须执行 X”时,都意味着如果它没有执行,则会抛出 TypeError
。一些不变式限制返回值,而另一些则限制参数。陷阱返回值的正确性通过两种方式确保:通常,非法值意味着抛出 TypeError
。但是,每当需要布尔值时,都会使用强制转换将非布尔值转换为合法值。
这是强制执行的不变式的完整列表
apply(target, thisArgument, argumentsList)
construct(target, argumentsList, newTarget)
null
或原始值)。defineProperty(target, propKey, propDesc)
propKey
必须是目标的自有键之一。propDesc
将属性 configurable
设置为 false
,则目标必须具有一个不可配置的自有属性,其键为 propKey
。propDesc
为目标(重新)定义自有属性,则不得导致异常。如果属性 writable
和 configurable
禁止更改,则会抛出异常(不可扩展性由第一条规则处理)。deleteProperty(target, propKey)
get(target, propKey, receiver)
propKey
,则处理程序必须返回该属性的值。undefined
。getOwnPropertyDescriptor(target, propKey)
undefined
。writable
和 configurable
不允许更改,则会抛出异常(不可扩展性由第三条规则处理)。因此,处理程序不能将不可配置的属性报告为可配置的,并且不能为不可配置的不可写属性报告不同的值。getPrototypeOf(target)
null
。has(target, propKey)
isExtensible(target)
target.isExtensible()
相同。ownKeys(target)
preventExtensions(target)
target.isExtensible()
之后必须为 false
。set(target, propKey, value, receiver)
propKey
,则 value
必须与该属性的值相同(即,该属性不能被更改)。TypeError
(即,不能设置此类属性)。setPrototypeOf(target, proto)
proto
必须与目标的原型相同。否则,将抛出 TypeError
。普通对象的以下操作会在原型链中的对象上执行操作。因此,如果该链中的某个对象是代理,则会触发其陷阱。规范将这些操作实现为内部自身方法(JavaScript 代码不可见)。但在本节中,我们假设它们是与陷阱同名的普通方法。参数 target
成为方法调用的接收者。
target.get(propertyKey, receiver)
target
没有具有给定键的自身属性,则会在 target
的原型上调用 get
。target.has(propertyKey)
get
类似,如果 target
没有具有给定键的自身属性,则会在 target
的原型上调用 has
。target.set(propertyKey, value, receiver)
get
类似,如果 target
没有具有给定键的自身属性,则会在 target
的原型上调用 set
。所有其他操作仅影响自身属性,它们对原型链没有影响。
全局对象 Reflect
将 JavaScript 元对象协议的所有可拦截操作实现为方法。这些方法的名称与处理程序方法的名称相同,正如我们所见,这有助于将操作从处理程序转发到目标。
Reflect.apply(target, thisArgument, argumentsList) : any
Function.prototype.apply()
相同。Reflect.construct(target, argumentsList, newTarget=target) : Object
new
运算符作为函数。target
是要调用的构造函数,可选参数 newTarget
指向启动当前构造函数调用链的构造函数。有关 ES6 中如何链接构造函数调用的更多信息,请参阅关于类的章节。Reflect.defineProperty(target, propertyKey, propDesc) : boolean
Object.defineProperty()
类似。Reflect.deleteProperty(target, propertyKey) : boolean
delete
运算符作为函数。不过,它的工作方式略有不同:如果它成功删除了属性或该属性从未存在,则返回 true
。如果无法删除该属性并且该属性仍然存在,则返回 false
。保护属性不被删除的唯一方法是使其不可配置。在松散模式下,delete
运算符返回相同的结果。但在严格模式下,它会抛出 TypeError
而不是返回 false
。Reflect.get(target, propertyKey, receiver=target) : any
get
在原型链的后面到达 getter 时,需要可选参数 receiver
。然后它为 this
提供值。Reflect.getOwnPropertyDescriptor(target, propertyKey) : PropDesc|Undefined
Object.getOwnPropertyDescriptor()
相同。Reflect.getPrototypeOf(target) : Object|Null
Object.getPrototypeOf()
相同。Reflect.has(target, propertyKey) : boolean
in
运算符作为函数。Reflect.isExtensible(target) : boolean
Object.isExtensible()
相同。Reflect.ownKeys(target) : Array<PropertyKey>
Reflect.preventExtensions(target) : boolean
Object.preventExtensions()
类似。Reflect.set(target, propertyKey, value, receiver=target) : boolean
Reflect.setPrototypeOf(target, proto) : boolean
__proto__
。几种方法具有布尔结果。对于 has
和 isExtensible
,它们是操作的结果。对于其余方法,它们指示操作是否成功。
Reflect
使用案例 除了转发操作之外,为什么 Reflect
很有用 [4]?
Reflect
复制了 Object
的以下方法,但其方法返回布尔值,指示操作是否成功(其中 Object
方法返回已修改的对象)。Object.defineProperty(obj, propKey, propDesc) : Object
Object.preventExtensions(obj) : Object
Object.setPrototypeOf(obj, proto) : Object
Reflect
方法实现的功能只能通过运算符获得Reflect.construct(target, argumentsList, newTarget=target) : Object
Reflect.deleteProperty(target, propertyKey) : boolean
Reflect.get(target, propertyKey, receiver=target) : any
Reflect.has(target, propertyKey) : boolean
Reflect.set(target, propertyKey, value, receiver=target) : boolean
apply()
的较短版本:如果您想完全安全地在函数上调用方法 apply()
,则不能通过动态调度来执行此操作,因为该函数可能具有键为 'apply'
的自身属性
使用 Reflect.apply()
更短
delete
运算符会在严格模式下抛出异常。在这种情况下,Reflect.deleteProperty()
返回 false
。Object.*
与 Reflect.*
展望未来,Object
将托管对普通应用程序感兴趣的操作,而 Reflect
将托管更底层的操作。
至此,我们对代理 API 的深入研究就结束了。对于每个应用程序,您都必须考虑性能,并在必要时进行测量。代理可能并不总是足够快。另一方面,性能通常并不重要,并且拥有代理提供的元编程能力是很好的。正如我们所见,它们可以帮助解决许多用例。
[1] Tom Van Cutsem 和 Mark Miller 的“ECMAScript 反射 API 的设计”。技术报告,2012 年。[本章的重要来源。]
[2] Gregor Kiczales、Jim des Rivieres 和 Daniel G. Bobrow 的“元对象协议的艺术”。书籍,1991 年。
[3] Ira R. Forman 和 Scott H. Danforth 的“让元类发挥作用:面向对象编程的新维度”。书籍,1999 年。
[4] Tom Van Cutsem 的“Harmony-reflect:我为什么要使用这个库?”。[解释了为什么 Reflect
很有用。]