【译】JavaScript元编程之—— Proxy简介

356 阅读5分钟

「这是我参与2022首次更文挑战的第13天,活动详情查看:2022首次更文挑战」。

前言

在之前的文章中 【译】JavaScript元编程简介 - 掘金 (juejin.cn)我们介绍了元编程其中之一的概念:intercession。intercession表示的是拦截某种操作。例如,你想要获取一个对象的属性值,但是该操作被JavaScript程序所拦截并且返回给了你一个另外的值。

intercession类似于“中间人”攻击,就是说数据在传输的过程中被中间人所创篡改,最终将被篡改过的信息送达。这个在数据传递过程中修改数据的程序就被称为 Proxy

Proxy

你可能已经听说过这个术语 “代理”,通过在你和远程服务器之间的代理服务器连接到远程服务器。这里,代理服务器既能够从远程服务器隐藏您的身份,也能转换从远程服务器接收的数据。

JavaScript在ES2015+的版本中为我们提供了 Proxy 这个类。Proxy 全局对象是一个构造函数(类),它的函数签名如下:

var proxy = new Proxy(target, handler);

这里, target 对象就是需要被 Proxy 所代理的对象。proxy 是被用于在target上执行某些操作的对象。例如: proxy.prop = 1 的操作实际上会被 handler 执行,将 target.prop 的值设为1.

为target创建proxy不会阻止访问target,但它应该保持私密。handler对象包含一些特定方法,当某些操作在proxy上执行时,其中一个将通过JavaScript调用并负责与target通信。

在之前的文章中,我们了解到“内插槽”和“内部方法”的概念。每个 proxy 对象都具有两个内插槽 [[ProxyTarget]][[ProxyHandler]] 。当我们使用new Proxy 来创建 proxy 对象时,这些插槽就会分别被 targethandler 所填充。

image.png

在介绍Reflect的文章中【译】JavaScript元编程之——Reflect API简介 - 掘金 (juejin.cn)我们学习到了Object的一些基本内部方法(如上图所示)。这些内部方法会被Reflect的静态方法所触发。假设 proxy 对象也有相同的内部方法,那么他们的函数签名就会保持一致(如下图所示)。

image.png

proxy 的内部方法被调用时,它实际上会调用target 相同的内部方法。例如,当 proxy.prop被访问时,那么proxy[[Get]]() 就会被调用了,这也就会触发 target[[Get]]()内部方法。由于 proxy没有执行任何的操作, 这也被称为无操作转发(no-op forwarding)。

然而,我们也使用 handler 来为proxy 的内部方法提供一个具体的实现。在上表中的右列中,这些方法名就是我们可以用作拦截方法。比如: handler.get 方法会拦截到[[Get]]内部方法的调用。

这些handler方法被称为trap,因为它拦截proxy的操作,因此也拦截到target本身的操作。此方法将target作为handler的第一个参数,并且根据内部方法规范接收参数的剩余部分。

如果你读过这篇文章【译】JavaScript元编程之——Reflect API简介 - 掘金 (juejin.cn),那么你就会知道 Reflect的静态方法会调用target 对象中的内部方法。所以 Reflect.get(target, prop, receiver) 会调用 target[[Get]](prop, receiver) 的内部方法,并返回 target 对象的 prop 属性。

我们的handler接受的参数与你调用Reflect静态方法的参数一致。所以在这个例子中,handler.get 会接受 target, prop, receiver 作为函数参数。现在如何与 target 之间保持联系以及返回什么值完全取决于你自己了。

这也意味着您可以在handler 中调用等同的 Reflect 方法,并将所有参数传递给Reflect方法而无需检查。这就是为什么Reflect的静态方法和 Proxy的handler方法共享相同函数签名的原因。

get & set 拦截器

让我们为一个包含有agename 属性的对象创建一个 proxyname 属性包含有lnamefname 来表示一个人的名字。我们想要使 name 属性看起来是一个字符串而不是一个对象。换句话说就是:当用户读取它时,name 属性是一个字符串,当用户写入 name 属性时,我们会将其分割为 fnamelname 两部分。

image.png

在上面的例子中,我们为 target 对象创建了一个包含有 getset 方法拦截器的 proxy 对象。无论什么是由我们执行 proxy.prop 方法或者 Reflect.get(proxy, ...) 方法时,get 拦截器都会被执行。对于set 操作也是类似的。

preventExtensions 拦截器

使用 proxy ,你可以阻止 target 对象被任意的属性注入。为了实现这一点,我们可以使用 prevenetExtensions 拦截器。

image.png

在上面的例子中, 当 Object.preventExtensions 或者 Reflect.preventExtensions方法被调用时 preventExtensions 拦截器就会执行。在该拦截器中,我们使用了 Object.freeze() 方法使 target 冻结,这意味着我们不能够往target 对象上添加任何新属性。

construct 拦截器

我们可以使用 proxy 来实现一个单例。

image.png

在上面的例子中,我们为 Person 类创建了一个 可以拦截构造器的PersonProxy。这意味着当new PersonProxy或者 Reflect.construct 被调用时,constrcut 拦截器会执行。

可撤销的proxy (Revocable proxy)

revocable proxy 是一个包含有 proxyrevoke的对象。proxy 就是上面我们提到的proxy, 而 revoke 是一个函数,它可以将proxy[[ProxyTarget]][[ProxyHandler]] 的内插槽的值设置为 null。因此,在调用 revoke 之后,在 proxy 对象上的任何操作都会抛出一个 TypeError

var {proxy, revoke} = Proxy.revocable(target, handler);

Proxy.revocable 方法返回一个可撤销的proxy 对象。当你使用一个第三方的API,这个API需要读取你提供的对象中的一些属性,而你需要当第三方API读取该属性时进行一些拦截操作,那么使用 revocable proxy 是一个理想的选择。一旦你不想要第三方的API对 target 进行修改,你就可以调用 revoke方法。

image.png

从上面的图中你可以看到,一旦 revoke 方法被调用,无论在handler上是否具有该拦截器,后续在 proxy 上的任何操作都会抛出 TypeError 异常。

原文地址:

Introduction to “Proxy” API for Metaprogramming in JavaScript | by Uday Hiwarale | JsPoint | Medium