代理和反射是 ES6 新增的两个特性。他们为开发者提供了拦截对象基本操作并向其嵌入额外行为的能力。可以给目标对象定义一个关联的代理对象,而这个代理对象可以作为抽象的目标对象来使用。在对目标对象的各种操作影响目标对象之前,可以在代理对象中对这些操作加以控制。本文内容为详读《JavaScript 高级程序设计(第4版)》的笔记。
在 ES6 之前,ECMAScript 中并没有类似代理的特性。它是一种新的基础性语言能力,很多转译程序都无法将其转为 ES6 之前的兼容性代码。所以只能在支持他们的平台上使用,如果不支持则需提供后备代码或者不能使用代理和反射。
代理是使用 Proxy 构造函数创建的。这个构造函数接收两个参数:目标对象和处理程序对象。这两个参数是必需的,而且是 Object 类型数据,不传或者数据类型不对都会抛出 TypeError 。如下列示例代码创建了一个处理程序对象为空对象的代理 proxy 。
const target = { id: 'target' };
const handler = {};
const proxy = new Proxy(target, handler);
// 对代理对象的操作会同时放映在两个对象上
proxy.name = 'object name';
console.log(proxy.name); // 'object name'
console.log(target.name); // 'object name'
// 对目标对象的操作也会同时反映在两个对象上(会绕过代理施予的行为)
target.id = 'object id';
console.log(proxy.id); // 'object id'
console.log(target.id); // 'object id'
// 可以打印两个对象,其实是一样的
console.log(target); // {id: 'object id', name: 'object name'}
console.log(proxy); // Proxy {id: 'object id', name: 'object name'}
// 但是他们的引用是不同的
console.log(target == proxy); // false
// 代理对象不能使用 instanceof 操作符(没有原型对象或者说原型是 undefined)
console.log(proxy instanceof Proxy);
// TypeError: Function has non-object prototype 'undefined' in instanceof check
捕获器即在处理程序对象中定义的“基本操作的拦截器”。每个处理程序对象中可以定义 0 个或多个捕获器,每个捕获器都对应一种基本操作,可以直接或者间接在代理对象上调用。每次代理对象上调用这些操作时,代理可以在这些操作传播到目标对象之前先调用捕获器函数,从而拦截并修改相应的行为。
const target = { id: 'target', name: 'tkop' };
const handler = {
set(target, prototype, value, receiver) {
target[prototype] = 'proxy_' + value;
console.log(receiver)// Proxy {id: 'proxy_id', name: 'tkop'}
},
};
const proxy = new Proxy(target, handler);
proxy.id = 'id';
target.name = 'name';
console.log(proxy.id); // 'proxy_id'
console.log(target.id); // 'proxy_id'
console.log(proxy.name); // 'name'
console.log(target.name); // 'name'
当通过代理对象执行 set 操作时,会触发定义的 set() 捕获器。注意这里的 set() 不是 ECMAScript 对象可以调用的方法。触发该捕获器后修改了赋值的行为。但是直接通过目标对象取执行操作时不会触发捕获器,该操作仍然会产生正常的行为。
所有的捕获器都可以访问相应的参数,基于这些参数可以重建被捕获方法(触发操作)的原始行为。比如上面的 set() 捕获器会接收到目标对象、引用的目标对象上的字符串键属性、赋给属性的新值和接收最初赋值的对象四个参数。
但并非所有的捕获器行为都像 set() 或者 get() 那么简单。有些很难去手动重建原始行为,此时可以通过调用全局 Reflect 对象上(封装了原始行为)的同名方法进行重建。
const target = { id: 'target', name: 'tkop' };
const handler = {
set(target, prototype, value, receiver) {
// 修改参数行为后直接调用 Reflect 相应方法。
// 无需关系该方法内部详细的操作
value += '_set';
return Reflect.set(...arguments);
},
};
const proxy = new Proxy(target, handler);
proxy.id = 'id';
console.log(proxy.id); // 'id_set'
console.log(target.id); // 'id_set'
处理程序对象中所有可以捕获的方法都有对象的反射(Reflect)API 方法。这些方法与捕获器拦截的方法具有相同的名称和行为。具体有哪些可以查看 MDN(内置对象 Reflect)。因此如果需要定义的是一个空代理对象(不定义任何捕获器或者定义的捕获器没有重建被捕获方法的原始行为),那么可以写得非常简洁。
const target = { id: 'target', name: 'tkop' };
const handler = {
set() {
return Reflect.set(...arguments);
},
// 甚至可以直接这么写
get: Reflect.get,
};
const proxy = new Proxy(target, handler);
proxy.id = 'id';
console.log(proxy.id); // 'id'
console.log(target.id); // 'id'
// 可以捕获所有操作,但是不重建,只是将每个方法转发给对应反射 API 的空代理
const target1 = { id: 'target', name: 'tkop' };
const proxy1 = new Proxy(target, Reflect);
对象调用某些操作时是有相应的行为限制的,相应的代理中的捕获器也应遵循这些限制。例如目标对象返回一个只读且不可配置的数据属性时,需要返回实际的属性值,否则会报错。这就是捕获器的不变式,他们因操作(方法)不同而异。
const target = { id: 'target' };
Object.defineProperty(target, 'name', {
configurable: false,
writable: false,
value: 'target_name',
});
const handler = {
get() {
return Reflect.get(...arguments) + '_get';
// return Reflect.get(...arguments);
},
};
const proxy = new Proxy(target, handler);
proxy.name = 'name';
console.log(proxy.name); // 报错(Uncaught TypeError)
// 在目标对象中 name 属性是只读和不可配置属性,但是代理没有返回它实际的值
使用 new Proxy() 创建的普通代理对象与目标对象之间的联系会在代理对象的生命周期内一直持续存在。某些时候可能需要中断代理对象与目标对象之间的联系(但是代理对象依旧存在)。这就需要使用 Proxy.revocable() 方法来创建代理对象。
const target = { id: 'target' };
const handler = {
get() {
return Reflect.get(...arguments) + '_get';
},
};
const { proxy: p1, revoke: p1Revoke } = Proxy.revocable(target, handler);
console.log(p1.id); // 'target_get'
p1Revoke();
// Proxy { [[Handler]]: null, [[Target]]: null, [[IsRevoked]]: true }
console.log(p1);
// Uncaught TypeError: Cannot perform 'get' on a proxy that has been revoked
console.log(p1.id);
静态方法 Proxy.revocable() 除了对外暴露与目标对象建立联系的可撤销代理对象 proxy 外,还提供了相应的撤销函数 revoke() 。只需要调用该函数就可以中断相应的代理对象和目标对象之间的联系。撤销的操作是不可逆的,一旦撤销就不可再次建立联系。撤销函数是幂等的,调用多少次都一样。撤销代理后再调用代理会抛出 TypeError。
某些情况下,我们应该优先使用反射 API 。
1、反射 API 与对象 API
反射 API 是非常灵活的,它并不局限于捕获处理程序。大多数反射 API 方法在 Object 类型上有对应的方法(例如 defineProperty、getPrototypeOf 等)。通常,Object 上的方法适用于通用程序,而反射方法适用于细粒度的对象控制与操作。
2、合理利用反射方法返回的操作状态
很多反射方法会返回表示意图执行的操作是否成功的布尔值。合理利用这些布尔值(状态标记)能够帮助我们写出更易维和的代码。例如使用 Reflect.defineProperty() 来代替 Object.defineProperty() 。如果定义属性时发生问题,前者只会返回 false 标记本次操作失败,而后者会抛出错误。
try {
Object.defineProperty(myObj, 'name', 'tkop');
console.log('success');
} catch (e) {
console.log('failure');
}
console.log(Reflect.defineProperty(myObj, 'name', { value: 'tkop' }) ? 'success' : 'failure');
以下反射方法都会返回状态标记:
3、利用一等函数替代操作符
4、提供更加安全的方法调用
需要调用某个函数的 apply 方法时,该函数可能定义了实例方法 apply 。此时为了安全而绕过这个问题,就需要调用原型对象上的 apply 方法。但是如果直接使用 Reflect.apply() 来代替逻辑则会更加清晰。
Function.prototype.apply.call(func, thisVal, argumentList);
// 使用反射 API
Reflect.apply(func, thisVal, argumentsList);
1、可以通过设置一个代理去代理另一个代理,这样就可以在一个目标对象上构建多层拦截网。
const target = { id: 0 };
const proxy1 = new Proxy(target, {
get(target, property) {
console.log('proxy1');
return Reflect.get(...arguments) === 'undefined' ? 'default' : Reflect.get(...arguments);
},
});
const proxy2 = new Proxy(proxy1, {
get(target, property) {
console.log('proxy2');
return property + ':' + Reflect.get(...arguments);
},
});
console.log(proxy1.id);
console.log(proxy2.id);
console.log(proxy2.name);
2、代理是在 ECMAScript 现有基础上构建起来的一套新的 API,虽然其实现已经做到最好了。很大程度上,代理作为对象的虚拟层可以正常使用。但是在某些情况下,代理也不能与现在的 ECMAScript 机制很好地协同。例如 this 指向的问题。
// 1、使用 weakMap 保存实例私有变量
const vm = new WeakMap();
class User {
constructor(id) {
vm.set(this, id);
}
set userId(id) {
vm.set(this, id);
}
get userId() {
return vm.get(this);
}
}
const user1 = new User(0);
console.log(user1.userId);
// 通过代理对象获取 userId 的值
// User 实例一开始使用目标对象作为 weakMap 中的键来保存私有变量
// 代理对象却尝试用自身作为键获取这个值,所以是 undefined
const proxyUser1 = new Proxy(user1, {});
console.log(proxyUser1.userId);
// 2、使用代理对象调用目标对象上的方法时,涉及到 this 的问题都应注意
user1.say = function () {
console.log(this === proxyUser1);
};
user1.say(); // false
proxyUser1.say(); // true
// 3、第一个问题解决方案
const UserProxy = new Proxy(User, {});
const proxyUser2 = new UserProxy(1);
console.log(proxyUser2.userId);
3、代理与内部槽位
代理与内置引用类型(例如 Array)的实例通常可以很好地协同,但是有些 ECMAScript 内置类型可能会依赖代理无法控制地机制,结果导致在代理上调用某些方法会出错。
const date = new Date();
const proxy = new Proxy(date, {});
console.log(proxy instanceof Date); // true
proxy.getDate(); // Uncaught TypeError: this is not a Date object.
Date 类型方法的执行依赖 this 值上的内部槽位 [[NumberDate]] 。代理对象上不存在这个内部槽位,而且这个内部槽位的值也不能通过普通的 get() 和 set() 操作访问到,于时代理拦截后本应转发给目标对象的方法会抛出 TypeError。
简单理解:本质上跟第二点的问题时一样的。这个所谓的槽位是日期引用类型的私有变量,外部或者说开发人员无法访问。代理对象调用方法时,执行获取该私有变量的操作,但是由于此时 this 是代理对象,无法获取到私有变量 [[NumberDate]]。方法无法继续执行相关操作,所以报错。调用期约 Promise 的方法也是如此。
const promise = new Promise(resolve => setTimeout(resolve, 3000, 'resolve'));
const promise1 = new Proxy(promise, {});
console.log(Reflect.ownKeys(promise1.__proto__));
promise1.then(x => console.log(x));
// TypeError: Method Promise.prototype.then called on incompatible receiver [object Object]
// 方法Promise.prototype.then在不兼容的接收器上调用[object对象]
代理可以捕获 13 中不同的基本操作。这些操作有各自不同的反射 API 方法、参数、关联 ECMAScript 操作和不变式。同一个捕获器可以捕获几种不同的操作,但是对于同一种操作,只会有一个捕获处理程序被调用。不会存在重复捕获的情况。
get() 捕获器会在获取属性值的操作中被调用。对应的反射 API 方法为 Reflect.get()。
const myTarget = {};
const proxy = new Proxy(myTarget, {
get(target, property, receiver) {
console.log('_get');
return Reflect.get(...arguments);
},
});
proxy.id; // '_get'
1、返回值
返回值无限制。
2、拦截的操作
3、捕获器处理程序参数
4、捕获器不变式
如果 target.property 不可写且不可配置,则处理程序的返回值必须与 target.property 匹配。如果 target.property 不可配置且 [[Get]] 特性为 undefined,处理程序的返回值也必须是 undefined。
set() 捕获器会在设置属性值的操作中被调用。对应的反射 API 方法为 Reflect.set()。
const myTarget = {};
const proxy = new Proxy(myTarget, {
set(target, property, value, receiver) {
console.log('_set');
return Reflect.set(...arguments);
},
});
proxy.id = 'tkop'; // '_set'
1、返回值
返回 true 表示成功;返回 false 表示失败,严格模式下会抛出 TypeError。
2、拦截的操作
3、捕获器处理程序参数
4、捕获器不变式
如果 target.property 不可写且不可配置,则不能修改目标属性的值。如果 target.property 不可配置且 [[Set]] 特性为 undefined,则不能修改目标属性的值。严格模式下,处理程序会返回 false 且抛出 TypeError。
has() 捕获器会在 in 操作符中被调用。对应的反射 API 方法为 Reflect.has()。
const myTarget = {};
const proxy = new Proxy(myTarget, {
has(target, property) {
console.log('_has');
return Reflect.has(...arguments);
},
});
'id' in proxy; // '_has'
1、返回值
has() 必须返回布尔值,表示属性是否存在。返回非布尔值会被转型为布尔值。
2、拦截的操作
3、捕获器处理程序参数
4、捕获器不变式
如果 target.property 存在且不可配置,则处理程序必须返回 true。如果 target.property 存在且目标对象不可扩张,则处理程序必须返回 true。
defineProperty() 捕获器会在 Object.defineProperty() 中被调用。对应的反射 API 方法为 Reflect.defineProperty()。
const myTarget = {};
const proxy = new Proxy(myTarget, {
defineProperty(target, property, descriptor) {
console.log('_defineProperty');
return Reflect.defineProperty(...arguments);
},
});
Object.defineProperty(proxy, 'id', { value: 'tkop' }); // '_defineProperty'
1、返回值
defineProperty() 必须返回布尔值,表示属性是否成功定义。返回非布尔值会被转型为布尔值。
2、拦截的操作
3、捕获器处理程序参数
4、捕获器不变式
如果目标对象不可扩展,则无法定义属性。如果目标对象上有一个可配置的属性,则不能添加同名的不可配置属性。如果目标对象上有一个不可配置的属性,则不能添加同名的可配置属性。
getOwnPropertyDescriptor() 捕获器会在 Object.getOwnPropertyDescriptor() 中被调用。对应的反射 API 方法为 Reflect.getOwnPropertyDescriptor()。
const myTarget = {};
const proxy = new Proxy(myTarget, {
getOwnPropertyDescriptor(target, property) {
console.log('_getOwnPropertyDescriptor');
return Reflect.getOwnPropertyDescriptor(...arguments);
},
});
Object.defineProperty(proxy, 'id', { value: 'tkop' });
console.log(Object.getOwnPropertyDescriptor(proxy, 'id')); // '_getOwnPropertyDescriptor'
// {
// value: 'tkop',
// writable: false,
// enumerable: false,
// configurable: false
// }
1、返回值
defineProperty() 必须返回属性描述对象,或者属性不存在时返回 undefined。
2、拦截的操作
3、捕获器处理程序参数
4、捕获器不变式
deleteProperty() 捕获器会在 delete 操作符中被调用。对应的反射 API 方法为 Reflect.deleteProperty()。
const myTarget = {};
const proxy = new Proxy(myTarget, {
deleteProperty(target, property) {
console.log('_deleteProperty');
return Reflect.deleteProperty(...arguments);
},
});
console.log(delete proxy.id); // '_deleteProperty' true
1、返回值
deleteProperty() 必须返回布尔值,表示删除属性是否成功。返回非布尔值会被转型为布尔值。
2、拦截的操作
3、捕获器处理程序参数
4、捕获器不变式
如果自有的 target.property 存在且不可配置,则处理程序不能删除这个属性。
ownKeys() 捕获器会在 Object.keys() 及类似方法中被调用。对应的反射 API 方法为 Reflect.ownKeys()。
const myTarget = { id: 'tkop' };
const proxy = new Proxy(myTarget, {
ownKeys(target) {
console.log('_ownKeys');
return Reflect.ownKeys(...arguments);
},
});
console.log(Object.keys(proxy)); // '_ownKeys' [ 'id' ]
1、返回值
ownKeys() 必须返回包含字符串或者符号的可枚举对象。
2、拦截的操作
3、捕获器处理程序参数
4、捕获器不变式
返回的可枚举对象必须包含 target 的所有不可配置的自有属性。如果 target 不可扩展,则返回可枚举对象必须准确地包含自有属性键。
getPrototypeOf() 捕获器会在 Object.getPrototypeOf() 中被调用。对应的反射 API 方法为 Reflect.getPrototypeOf()。
const myTarget = {};
const proxy = new Proxy(myTarget, {
getPrototypeOf(target) {
console.log('_getPrototypeOf');
return Reflect.getPrototypeOf(...arguments);
},
});
Object.getPrototypeOf(proxy); // '_getPrototypeOf'
console.log(proxy instanceof Object); // '_getPrototypeOf' true
1、返回值
getPrototypeOf() 必须返回对象或者 null。
2、拦截的操作
3、捕获器处理程序参数
4、捕获器不变式
如果 target 不可扩展,则 Object.getPrototypeOf(proxy) 唯一有效的返回值就是 Object.getPrototypeOf(target) 的返回值。
setPrototypeOf() 捕获器会在 Object.setPrototypeOf() 中被调用。对应的反射 API 方法为 Reflect.setPrototypeOf()。
const myTarget = {};
const arr = []
const proxy = new Proxy(myTarget, {
setPrototypeOf(target, prototype) {
console.log('_setPrototypeOf');
return Reflect.setPrototypeOf(...arguments);
},
});
Object.setPrototypeOf(proxy, arr); // '_setPrototypeOf'
console.log(proxy instanceof Array); // true
1、返回值
setPrototypeOf() 必须返回布尔值,表示原型赋值是否成功。返回非布尔值会被转型为布尔值。
2、拦截的操作
3、捕获器处理程序参数
4、捕获器不变式
如果 target 不可扩展,则唯一有效的 prototype 参数就是 Object.getPrototypeOf(target) 的返回值。
isExtensible() 捕获器会在 Object.isExtensible() 中被调用。对应的反射 API 方法为 Reflect.isExtensible()。
const myTarget = {};
const proxy = new Proxy(myTarget, {
isExtensible(target) {
console.log('_isExtensible');
return Reflect.isExtensible(...arguments);
},
});
console.log(Object.isExtensible(proxy)); // '_isExtensible' true
1、返回值
isExtensible() 必须返回布尔值,表示 target 是否可扩展。返回非布尔值会被转型为布尔值。
2、拦截的操作
3、捕获器处理程序参数
4、捕获器不变式
如果 target 不可扩展,则处理程序必须返回 false。
如果 target 可扩展,则处理程序必须返回 true。
preventExtensions() 捕获器会在 Object.preventExtensions() 中被调用。对应的反射 API 方法为 Reflect.preventExtensions()。
const myTarget = {};
const proxy = new Proxy(myTarget, {
preventExtensions(target) {
console.log('_preventExtensions');
return Reflect.preventExtensions(...arguments);
},
});
console.log(Reflect.preventExtensions(proxy)); // '_preventExtensions' true
1、返回值
preventExtensions() 必须返回布尔值,表示 target 是否成功被设置为不可扩展。返回非布尔值会被转型为布尔值。
这里自己测试使用 Object.preventExtensions(proxy) 返回的是不可扩展的 proxy(代理对象),并不是布尔值。
2、拦截的操作
3、捕获器处理程序参数
4、捕获器不变式
如果 Object.isExtensible(proxy) 返回的是 false,则处理程序必须返回 true。
apply() 捕获器会在调用目标函数时被调用。对应的反射 API 方法为 Reflect.apply()。
const myTarget = function () {};
const proxy = new Proxy(myTarget, {
// 书中将 argunmentsList 进行扩展,个人认为非常没必要。
// 这样会使得 argumentsList 变为二维数组,不易使用
apply(target, thisArg, argumentsList) {
console.log('_apply');
return Reflect.apply(...arguments);
},
});
proxy(); // '_apply'
1、返回值
返回值没有限制。
2、拦截的操作
3、捕获器处理程序参数
4、捕获器不变式
target 必须是一个函数对象。
construct() 捕获器会在 new 操作符中被调用。对应的反射 API 方法为 Reflect.construct()。
const Person = function (name) {
this.name = name;
};
const proxy = new Proxy(Person, {
construct(target, argumentsList, newTarget) {
console.log('_construct');
return Reflect.construct(...arguments);
},
});
const my = new proxy('tkop'); // '_construct'
1、返回值
construct() 返回值必须是一个对象。
2、拦截的操作
3、捕获器处理程序参数
4、捕获器不变式
目标对象必须可以用作构造函数。
通过使用代理在代码中实现一些有用的编程模式。书中提到的有关代理的编程模式如下,为了方便我将所有的示例代码进行了整合。
跟踪属性访问:通过捕获 get、set、has 等操作,可以知道对象属性什么时候被访问、查询。把实现相应捕获器的某个对象代理放到应用中,可以监控这个对象在何时何处被访问过。
隐藏属性:代理的内部实现对外部代码是不可见的,因此要隐藏目标对象上的属性也轻而易举。
属性验证:因为所有赋值操作都会触发 set() 捕获器,所以可以根据所赋的值决定是允许赋值还是拒绝赋值。
函数与构造函数参数验证:跟保护和验证对象属性类似,也可以对函数和构造函数参数进行审查。可以让函数只接收某类型的参数。
数据绑定与可观察对象:通过代理可以把运行时本不相关的部分联系到一起。例如可以将一个被代理的类绑定到一个全局实例集合,所有创建的实例都被添加到这个集合中。又例如把一个被代理的数组绑定到一个事件分派程序,代理的数组每次插入(修改)数据项时都会发送消息。
// 这里将数组视为目标对象(数组也是 Object 数据类型)
const targetArray = [301, 302];
// 事件分派程序,每次插入新值时都会发送消息
function emit(newValue) {
console.log(newValue);
}
const proxyArry = new Proxy(targetArray, {
get(target, property, receiver) {
// 跟踪属性访问
console.log(`Getting ${property}`);
// 隐藏属性
return ['0', '1'].includes(property) ? undefined : Reflect.get(...arguments);
},
set(target, property, value, receiver) {
console.log(`Setting ${property} = ${value}`);
// 属性验证
if (!(value instanceof User)) return false;
const setResult = Reflect.set(...arguments);
// 数据绑定与可观察对象
if (setResult) emit(Reflect.get(target, property, receiver));
return setResult;
},
has(target, property) {
// 隐藏属性
return ['0', '1'].includes(property) ? false : Reflect.has(...arguments);
},
});
const userList = [];
class User {
constructor(name) {
this.name = name;
}
}
const proxyUser = new Proxy(User, {
construct(target, argumentsList) {
// 构造函数参数验证
if (typeof argumentsList[0] !== 'string') {
throw 'The name must be a string';
}
const user = Reflect.construct(...arguments);
// 数据绑定与可观察对象
userList.push(user);
return user;
},
});
// 构造函数参数验证
const user1 = new proxyUser('user1');
// 构造函数参数验证(参数是一个数组会报错)
// const user2 = new proxyUser(userList);
// 数据绑定与可观察对象
console.log(userList);
// 属性验证、数据绑定与可观察对象(会执行 emit() 输出 user1)
proxyArry[2] = user1;
// 这个不会添加成功因为只能添加User类型的对象数据
proxyArry[3] = 404;
// 跟踪属性访问
console.log(proxyArry[2]);
console.log(proxyArry[3]);
// 隐藏属性
console.log(proxyArry[0]);
// 需要注意的是这里因为隐藏了索引为 0 和 1 的两个元素。
// 所以在获取数组的 length 属性时得到的是错误的值,使用 push 的话会报错
console.log(proxyArry.length);
const user3 = new proxyUser('user3');
// TypeError: 'set' on proxy: trap returned falsish for property 'length'
proxyArry.push(user3);