本文详解ES6中Proxy代理与Reflect反射API的核心机制。通过拦截get、set、apply等底层操作,开发者可自定义对象行为。文章涵盖属性验证、函数参数检查及类构造器重写等实战案例,展示如何精确控制JavaScript引擎的内置行为。
代理与反射的基本概念
Proxy(代理)能够拦截并修改 JavaScript 引擎的底层操作,涵盖数据读取、属性定义及函数构造等多个环节。ES6 借助这些底层内置对象的代理陷阱(Trap)与反射函数,赋予开发者更贴近引擎核心能力的控制力。
一、代理与反射的基本概念
如何理解代理与反射?
Proxy 用于替代另一个目标对象(target)。通过 new Proxy() 创建的代理,在表面上可与目标对象视为同一实体进行交互。
当对目标对象执行特定的底层操作时,代理允许拦截这些操作并进行覆写,这原本是 JavaScript 引擎的内部权限。
若对代理与反射的概念感到困惑,可直接参考后续的应用示例,阅读完后再回看定义,理解会更加清晰。
拦截行为依赖于响应特定操作的函数(称为陷阱),每个代理陷阱都对应一个 Reflect 方法。
ES6 的反射 API 以 Reflect 对象形式存在,其每个方法名与对应的陷阱函数一致,且接收参数也相同。以下是 Reflect 对象的部分方法:
| 代理陷阱 | 覆写的特性 | 方法 | |
|---|---|---|---|
| get | 读取一个属性的值 | Reflect.get() | |
| set | 写入一个属性 | Reflect.set() | |
| has | in 运算符 | Reflect.has() | |
| deleteProperty | delete 运算符 | Reflect.deleteProperty() | |
| getPrototypeOf | Object.getPrototypeOf() | Reflect.getPrototypeOf() | |
| isExtensible | Object.isExtensible() | Reflect.isExtensible() | |
| defineProperty | Object.defineProperty() | Reflect.defineProperty | |
| apply | 调用一个函数 | Reflect.apply() | |
| construct | 使用 new 调用一个函数 | Reflect.construct() |
每个陷阱函数均可重写 JavaScript 对象的特定内置行为,从而实现拦截与修改。
综上,若要控制或改变 JavaScript 的底层操作,需先创建代理对象并挂载陷阱函数,陷阱函数内部调用反射方法。结合后续应用示例,可更直观地理解代理过程。
创建简单的代理示例
二、开始一个简单的代理
使用 Proxy 构造器创建代理时,需传递两个参数:目标对象(target)与处理器(handler)。
以下示例创建一个仅进行透传的代理:
// 目标对象
let target = {};
// 代理对象
let proxy = new Proxy(target, {});
proxy.name = "hello";
console.log(proxy.name); // "hello"
console.log(target.name); // "hello"
target.name = "world";
console.log(proxy.name); // "world"
console.log(target.name); // "world
上述代码中,proxy 代理对象将所有操作直接传递给 target 目标对象。代理自身不存储属性,仅将值传递给 target,因此 proxy.name 与 target.name 的值始终相等,因为它们均指向 target.name。
此时代理陷阱的处理器为空对象,但处理器可定义一个或多个陷阱函数。
set 验证对象属性存储
2.1 set 验证对象属性的存储
假设需创建一个对象,要求其属性值仅限数值类型,这意味着该对象的每个新增属性
均需经过验证,若属性值非数值类型,则应抛出错误。
此时需使用 set 陷阱函数拦截传入的 value,该陷阱函数接受四个参数:
- trapTarget:接收属性的对象(即代理的目标对象)
- key:需写入的属性的键(字符串类型或符号类型)
- value:将被写入属性的值;
- receiver:操作发生的对象(通常为代理对象)
set 陷阱对应的反射方法及默认特性为 Reflect.set(),与陷阱函数一样接收这四个参数,并根据操作是否成功返回相应结果:
let targetObj = {};
let proxyObj = new Proxy(targetObj, {
set: set
});
/* 定义 set 陷阱函数 */
function set (trapTarget, key, value, receiver) {
if (isNaN(value)) {
throw new TypeError("Property " + key + " must be a number.");
}
return Reflect.set(trapTarget, key, value, receiver);
}
/* 测试 */
proxyObj.count = 123;
console.log(proxyObj.count); // 123
console.log(targetObj.count); // 123
proxyObj.anotherName = "proxy" // TypeError: Property anotherName must be a number.
示例中 set 陷阱函数成功拦截传入的 value 值,读者可尝试注释或不 return Reflect.set() 的情况,答案是拦截陷阱将无反射响应。
需注意,直接给 targetObj 目标对象赋值时不会触发 set 代理陷阱,仅通过给代理对象赋值才会触发 set 代理陷阱与反射。
get 验证对象属性读取
2.2 get 验证对象属性的读取
JavaScript 的一个有趣特性是,读取不存在的属性时不会抛出错误,而是将 undefined 视为该属性的值。
在大型代码库中,属性名称书写错误(不抛错)会导致严重问题。此时使用 get 代理陷阱验证对象结构(Object Shape),访问不存在的属性时抛出错误,使对象结构验证变得简单。
get 陷阱函数在读取属性时被调用,即使该属性在对象中不存在,它接受三个参数:
- trapTarget:将被读取属性的对象(即代理的目标对象)
- key:需读取的属性的键(字符串类型或符号类型)
- receiver:操作发生的对象(通常为代理对象)
Reflect.get() 方法接受相同参数,并返回默认属性的默认值。
let proxyObj = new Proxy(targetObj, {
set: set,
get: get
});
/* 定义 get 陷阱函数 */
function get(trapTarget, key, receiver) {
if (!(key in receiver)) {
throw new TypeError("Property " + key + " doesn't exist.");
}
return Reflect.get(trapTarget, key, receiver);
}
console.log(proxyObj.count); // 123
console.log(proxyObj.newcount) // TypeError: Property newcount doesn't exist.
这段代码允许添加新属性,且此后可正常读取该属性的值,但当读取的属性不
存在时,程序抛出错误,而非将其默认为 undefined。
还可使用 has 陷阱验证 in 运算符,使用 deleteProperty 陷阱函数防止属性被 delete 删除。 注:in 运算符用于判断对象中是否存在某个属性,若自有属性或原型属性匹配该名称字符串或 Symbol,则 in 运算符返回 true。
targetObj = {
name: 'targetObject'
};
console.log("name" in targetObj); // true
console.log("toString" in targetObj); // true
其中 name 是对象自身的属性,而 toString 则是原型属性(从 Object 对象继承而来),因此检测结果均为 true。
has 陷阱函数在使用 in 运算符时被调用,并传入两个参数(同名反射 Reflect.has() 方法亦然):
- trapTarget:需读取属性的对象(代理的目标对象)
- key:需检查的属性的键(字符串类型或 Symbol 符号类型)
deleteProperty 陷阱函数在使用 delete 运算符删除对象属性时被调用,并传入两个参数(Reflect.deleteProperty() 方法也接受这两个参数):
- trapTarget:需删除属性的对象(即代理的目标对象);
- key:需删除的属性的键(字符串类型或符号类型)。
一些思考:分析过 Vue 源码者应了解,Vue 实例中挂载的 data 是通过 Object.defineProperty 代理 vm._data 中的对象属性,实现双向绑定……同理,可考虑使用 ES6 的 Proxy 的 get 和 set 陷阱实现此代理。
对象属性陷阱详解
三、对象属性陷阱
3.1 数据属性与访问器属性
ES5 最重要的特征之一是引入 Object.defineProperty() 方法定义属性的特性。属性特性是为实现 JavaScript 引擎所用,属内部值,因此无法直接访问。
属性分为数据属性和访问器属性。使用 Object.defineProperty() 方法修改数据属性特性值的示例如下:
let obj1 = {
name: 'myobj',
}
/* 数据属性*/
Object.defineProperty(obj1,'name',{
configurable: false, // default true
writable: false, // default true
enumerable: true, // default true
value: 'jenny' // default undefined
})
console.log(obj1.name) // 'jenny'
其中 [[Configurable]] 表示能否通过 delete 删除属性从而重新定义为访问器属性;[[Enumerable]] 表示能否通过 for-in 循环返回属性;[[Writable]] 表示能否修改属性的值;[[Value]] 包含该属性的数据值。
对于访问器属性,该属性不包含数据值,包含一对 getter 和 setter 函数,定义访问器属性必须使用 Object.defineProperty() 方法:
let obj2 = {
age: 18
}
/* 访问器属性 */
Object.defineProperty(obj2,'_age',{
configurable: false, // default true
enumerable: false, // default true
get () { // default undefined
return this.age
},
set (num) { // default undefined
this.age = num
}
})
/* 修改访问器属性调用 getter */
obj2._age = 20
console.log(obj2.age) // 20
/* 输出访问器属性 */
console.log(Object.getOwnPropertyDescriptor(obj2,'_age'))
// { get: [Function: get],
// set: [Function: set],
// enumerable: false,
// configurable: false }
[[Get]] 在读取属性时调用的函数,[[Set]] 在写入属性时调用的函数。使用访问器属性的常用方式,是设置一个属性的值导致其他属性发生变化。
3.2 检查属性的修改
代理允许使用 defineProperty 同名函数陷阱函数拦截 Object.defineProperty() 的调用,defineProperty 陷阱函数接受下列三个参数:
- trapTarget:需被定义属性的对象(即代理的目标对象);
- key:属性的键(字符串类型或符号类型);
- descriptor:为该属性准备的描述符对象。
defineProperty 陷阱函数要求在操作后返回一个布尔值以判断操作是否成功,若返回 false 则抛出错误,故可使用该功能来限制哪些属性可被 Object.defineProperty() 方法定义。
例如,若想阻止定义 Symbol 符号类型的属性,可检查传入的属性值,若是则返回 false:
/* 定义代理 */
let proxy = new Proxy({}, {
defineProperty(trapTarget, key, descriptor) {
if (typeof key === "symbol") {
return false;
}
return Reflect.defineProperty(trapTarget, key, descriptor);
}
});
Object.defineProperty(proxy, "name", {
value: "proxy"
});
console.log(proxy.name); // "proxy"
let nameSymbol = Symbol("name");
// 抛出错误
Object.defineProperty(proxy, nameSymbol, {
value: "proxy"
})
四、函数代理
函数代理的应用
4.1 构造函数 & 立即执行
函数的两个内部方法:[[Call]] 与 [[Construct]] 会在函数被调用时执行,通过代理函数为这两个内部方法设置陷阱,从而控制函数的行为。
[[Construct]] 会在函数被使用 new 运算符调用时执行,代理触发 construct() 陷阱函数,并与 Reflect.construct() 一样接收下列两个参数:
- trapTarget:被执行的函数(即代理的目标对象);
- argumentsList:被传递给函数的参数数组。
[[Call]] 会在函数被直接调用时执行,代理触发 apply() 陷阱函数,它与 Reflect.apply() 均接收三个参数:
- trapTarget:被执行的函数(代理的目标函数);
- thisArg:调用过程中函数内部的 this 值;
- argumentsList:被传递给函数的参数数组。
每个函数都包含 call() 和 apply() 方法,用于重置函数运行的作用域即 this 指向,区别仅在于接收参数的方式不同:call() 的参数需逐个列举,apply() 是参数数组。
显然,apply 与 construct 要求代理目标对象必须是一个函数,这两个代理陷阱在函数的执行方式上开启了许多可能性,结合使用即可完全控制任意代理目标函数的行为。
验证函数参数
4.2 验证函数的参数
观察到 apply() 和 construct() 陷阱的参数中均有被传递给函数的参数数组 argumentsList,因此可用于验证函数的参数。
例如需保证所有参数均为特定类型,且不能通过 new 构造使用,示例如下:
/* 定义 sum 目标函数 */
function sum(...values) {
return values.reduce((previous, current) => previous + current, 0);
}
/* 定义 apply 陷阱函数 */
function applyRef (trapTarget, thisArg, argumentList) {
argumentList.forEach((arg) => {
if (typeof arg !== "number") {
throw new TypeError("All arguments must be numbers.");
}
});
return Reflect.apply(trapTarget, thisArg, argumentList);
}
/* 定义 construct 陷阱函数 */
function constructRef () {
throw new TypeError("This function can't be called with new.");
}
/* 定义 sumProxy 代理函数 */
let sumProxy = new Proxy(sum, {
apply: applyRef,
construct: constructRef
});
console.log(sumProxy(1, 2, 3, 4)); // 10
// console.log(sumProxy(1, "2", 3, 4)); // TypeError: All arguments must be numbers.
// let result = new sumProxy() // TypeError: This function can't be called with new.
sum() 函数会将所有传递进来的参数值相加,此代码通过将 sum() 函数封装在 sumProxy() 代理中,若传入参数的值非数值类型,该函数仍会尝试加法操作,但在函数运行之前拦截了函数调用,触发 apply 陷阱函数以保证每个参数均为数值。
出于安全考虑,这段代码使用 construct 陷阱抛出错误,以确保该函数不会被使用 new 运算符调用
实例对象 instance 对象会被同时判定为 proxy 与 target 对象的实例,是因为 instanceof 运算符使用了原型链来进行推断,而原型链查找并未受此代理影响,因此 proxy 对象与 target 对象对于 JavaScript 引擎而言具有相同的原型。
调用类的构造函数
4.3 调用类的构造函数
ES6 中新引入了 class 类的概念,类使用 constructor 构造函数封装数据,并规定必须始终使用 new 来调用,原因是类构造器的内部方法 [[Call]] 被明确要求抛出错误。
代理可拦截对于 [[Call]] 方法的调用,你可借助代理调用的类构造器。例如在缺少 new 的情况下创建一个新实例,就使用 apply 陷阱函数实现:
class Person {
constructor(name) {
this.name = name;
}
}
let PersonProxy = new Proxy(Person, {
apply: function(trapTarget, thisArg, argumentList) {
return new trapTarget(...argumentList);
}
});
let me = PersonProxy("Jenny");
console.log(me.name); // "Jenny"
console.log(me instanceof Person); // true
console.log(me instanceof PersonProxy); // true
类构造器即类的构造函数,使用代理时其行为如同函数,apply 陷阱函数重写了默认的构造行为。
关于类的更多有趣用法,可参考 【ES6】更易于继承的类语法
总结而言,代理的用途极为广泛,因其提供了修改 JavaScript 内置对象所有行为的入口。上述示例仅为简单应用入门,还有更多复杂示例,推荐阅读《深入理解 ES6》。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。