Get to know MDN better
Proxy 对象允许你创建一个可替代原始对象的对象,但该对象可能重定义获取、设置和定义属性等基础 Object 操作。代理对象常用于记录属性访问、验证、格式化或清理输入等场景。
创建 Proxy 需提供两个参数:
例如,此段代码为 target 对象创建了代理:
由于 handler 是空的,此代理的行为如同直接对源对象进行操作:
要自定义代理,我们在 handler 对象中定义函数:
这里我们提供了一个 get() 处理器的实现,它会拦截对目标对象属性访问的尝试。
处理器函数有时被称为陷阱,大概是因为它们会捕获对目标对象的调用。上文 handler2 中的陷阱重新定义了所有属性访问器:
代理常与 Reflect 对象配合使用,该对象提供了一些与 Proxy 陷阱同名的方法。Reflect 方法通过调用对应的对象内部方法来实现反射语义。例如,若不希望重定义对象行为,可调用 Reflect.get:
Reflect 方法仍通过对象内部方法与对象交互——若在代理上调用该方法,它不会“解除代理化”。若在代理陷阱中使用 Reflect 方法,且该方法调用再次被陷阱拦截,则可能引发无限递归。
以下术语用于描述代理的功能特性。
handler作为 Proxy 构造函数的第二个参数传递的对象。它包含定义代理行为的陷阱。
陷阱(trap)定义对应对象内部方法行为的函数(类似于操作系统中的陷阱概念。)
目标(target)代理虚拟化的对象。它通常作为代理的存储后端使用。关于对象不可扩展性或不可配置属性的不变性(保持不变的语义)将针对目标对象进行验证。
不变量在实现自定义操作时保持不变的语义。如果陷阱实现违反了处理器的不变性,将抛出 TypeError 异常。
对象是属性的集合。然而,该语言并未提供任何机制来直接操作对象中存储的数据——相反,对象定义了一些内部方法来规定其交互方式。例如,当你读取 obj.x 时,你可能会期望发生以下情况:
这种过程在语言中并无特殊之处——仅仅是因为普通对象默认具有一个名为 [[Get]] 的内部方法,该方法即以这种行为方式定义。obj.x 属性访问语法只是调用了对象的 [[Get]] 方法,而对象会通过自身内部方法的实现来决定返回什么内容。
另一个例子是,数组与普通对象不同,因为它们具有一个神奇的 length 属性——当修改该属性时,系统会自动为数组分配空槽位或移除元素。同样地,向数组添加元素会自动改变 length 属性。这是因为数组拥有 [[DefineOwnProperty]] 内部方法,该方法在写入整数索引时会更新 length,在写入 length 值时则更新数组内容。这类内部方法实现与普通对象不同的特殊对象被称为特殊对象。Proxy 使开发者能够全权定义自定义的特殊对象。
所有对象均具有以下内部方法:
| [[GetPrototypeOf]] | getPrototypeOf() |
| [[SetPrototypeOf]] | setPrototypeOf() |
| [[IsExtensible]] | isExtensible() |
| [[PreventExtensions]] | preventExtensions() |
| [[GetOwnProperty]] | getOwnPropertyDescriptor() |
| [[DefineOwnProperty]] | defineProperty() |
| [[HasProperty]] | has() |
| [[Get]] | get() |
| [[Set]] | set() |
| [[Delete]] | deleteProperty() |
| [[OwnPropertyKeys]] | ownKeys() |
函数对象还具有以下内部方法:
| [[Call]] | apply() |
| [[Construct]] | construct() |
需要认识到,与对象的所有交互最终都归结为调用这些内部方法之一,且所有方法均可通过代理进行定制。这意味着语言本身几乎不保证任何行为(除某些关键不变量外)——一切皆由对象自身定义。当执行 delete obj.x 时,无法保证后续执行 "x" in obj 会返回 false——这取决于对象对 [[Delete]] 和 [[HasProperty]] 方法的具体实现。delete obj.x 操作可能向控制台输出日志、修改全局状态,甚至可能定义新属性而非删除原有属性,尽管在编写代码时应避免此类语义行为。
所有内部方法均由语言本身调用,无法在 JavaScript 代码中直接访问。Reflect 命名空间提供的方法除执行输入规范化/验证外,主要功能就是调用这些内部方法。在每个陷阱的页面中,我们列出了触发该陷阱的典型场景,但这些内部方法在大量场景中被调用。例如数组方法通过这些内部方法读写数组,因此诸如 push() 之类的方法也会触发 get() 和 set() 陷阱。
大多数内部方法的功能都很直观。唯一可能令人混淆的是 [[Set]] 和 [[DefineOwnProperty]]。对于普通对象,前者会调用 setter;后者则不会(且当不存在属性或属性为数据属性时,[[Set]] 会内部调用[[DefineOwnProperty]]。)虽然你可能知道 obj.x = 1 语法使用 [[Set]],而 Object.defineProperty() 使用 [[DefineOwnProperty]],但其他内置方法和语法采用何种语义并不直观。例如,类字段 使用 [[DefineOwnProperty]] 语义,因此当派生类声明字段时,父类中定义的 setter 不会被调用。
创建一个新的 Proxy 对象。
备注:不存在 Proxy.prototype 属性,故 Proxy 的实例没有特殊的属性或方法。
创建一个可撤销的 Proxy 对象。
在以下简单的例子中,当对象中不存在属性名时,默认返回值为 37。下面的代码以此展示了 get() 处理器的使用场景。
在以下例子中,我们使用了一个原生 JavaScript 对象,代理会将所有应用到它的操作转发到这个对象上。
请注意,虽然这种“无操作”对普通 JavaScript 对象有效,但对原生对象(如 DOM 元素、Map 对象或任何具有内部槽的对象)无效。更多信息请参阅不转发私有字段。
代理仍是具有不同身份的另一个对象——它是在被封装对象与外部之间运作的代理。因此,代理无法直接访问原始对象的私有元素。
这是因为当代理的 get 陷阱被调用时,this 值是 proxy 而非原始的 secret,因此无法访问 #secret。要解决此问题,请将原始的 secret 作为 this 使用:
对于方法而言,这意味着你还需要将方法的 this 值重定向回原始对象:
某些原生 JavaScript 对象具有名为内部槽的属性,这些属性无法从 JavaScript 代码访问。例如,Map 对象拥有名为 [[MapData]] 的内部槽,用于存储映射的键值对。因此无法简单地为映射创建转发代理:
你必须使用上文所述的“this 恢复”代理来解决这个问题。
通过 Proxy,你可以轻松地验证向一个对象的传值。下面的代码借此展示了 set() 处理器的作用。
在此示例中,我们使用 Proxy 来切换两个不同元素的属性:当为一个元素设置该属性时,另一个元素的属性会被取消设置。
我们创建一个名为 view 的对象,该对象作为具有 selected 属性的对象的代理。代理处理器定义了 set() 处理器。
当我们将 HTML 元素赋值给 view.selected 时,该元素的 'aria-selected' 属性会被设置为 true。若随后将另一个元素赋值给 view.selected,则该元素的 'aria-selected' 属性会被设置为 true,而先前元素的 'aria-selected' 属性会自动设置为 false。
以下 products 代理会计算传值并根据需要转换为数组。这个代理对象同时支持一个叫做 latestBrowser 的附加属性,这个属性可以同时作为 getter 和 setter。
| ECMAScript® 2027 Language Specification # sec-proxy-objects |
启用 JavaScript 以查看此浏览器兼容性表。