ECMAScript 6新增代理和反射,可以给目标对象定义一个关联的代理对象,而这个代理对象可以作为抽象的目标对象来使用。在对
目标对象的各种操作影响目标对象之前,可以在代理对象中对这些操作加以控制。代理是一种新的基础性语言能力,很多转译程序都不能把代理行为转换为之前的 ECMAScript 代码,只在百分之百支持它们的平台上有用。
1 代理基础
代理类似 C++指针,因为它可以用作目标对象的替身,但又完全独立于目标对象。
注意 ECMAScript 代理与 C++指针有重大区别。指针在概念上易于理解。
1.1 创建空代理
代理是使用 Proxy 构造函数创建的。这个构造函数接收两个必要参数:目标对象和处理程序对象。如下面的代码所示,在代理对象上执行的任何操作实际上都会应用到目标对象。唯一可感知的不同就是代码中操作的是代理对象:
const target = {
id: 'target'
};
const handler = {};
const proxy = new Proxy(target, handler);
// id 属性会访问同一个值
console.log(target.id); // target
console.log(proxy.id); // target
// 给目标属性赋值会反映在两个对象上
// 因为两个对象访问的是同一个值
target.id = 'foo';
console.log(target.id); // foo
console.log(proxy.id); // foo
// 给代理属性赋值会反映在两个对象上
// 因为这个赋值会转移到目标对象
proxy.id = 'bar';
console.log(target.id); // bar
console.log(proxy.id); // bar
// hasOwnProperty()方法在两个地方
// 都会应用到目标对象
console.log(target.hasOwnProperty('id')); // true
console.log(proxy.hasOwnProperty('id')); // true
// Proxy.prototype 是 undefined
// 因此不能使用 instanceof 操作符
console.log(target instanceof Proxy); // TypeError: Function has non-object prototype
'undefined' in instanceof check
console.log(proxy instanceof Proxy); // TypeError: Function has non-object prototype
'undefined' in instanceof check
// 严格相等可以用来区分代理和目标
console.log(target === proxy); // false
1.2 定义捕获器
使用代理的主要目的是可以定义捕获器(trap)。捕获器就是在处理程序对象中定义的“基本操作的拦截器”。例如定义一个 get()捕获器:
const target = {
foo: 'bar'
};
const handler = {
// 捕获器在处理程序对象中以方法名为键
get() {
return 'handler override';
}
};
const proxy = new Proxy(target, handler);
console.log(target.foo); // bar
console.log(proxy.foo); // handler override
console.log(target['foo']); // bar
console.log(proxy['foo']); // handler override
console.log(Object.create(target)['foo']); // bar
console.log(Object.create(proxy)['foo']); // handler override
proxy[property]、proxy.property 或 Object.create(proxy)[property]等操作都会触发基本的 get()操作以获取属性。只有在代理对象上执行这些操作才会触发捕获器。在目标对象上执行这些操作仍然会产生正常的行为。
1.3 捕获器参数和反射 API
所有捕获器都可以访问相应的参数。比如,get()捕获器会接收到目标对象、要查询的属性和代理对象三个参数:
const target = {
foo: 'bar'
};
const handler = {
get(trapTarget, property, receiver) {
return trapTarget[property];
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.foo); // bar
console.log(target.foo); // bar
开发者并不需要手动重建原始行为,可以通过调用全局 Reflect 对象上(封装了原始行为)的同名方法来重建。处理程序对象中所有可以捕获的方法都有对应的反射(Reflect)API 方法。
const target = {
foo: 'bar'
};
const handler = {
get: Reflect.get
// 可以这样写
// get() {
// return Reflect.get(...arguments);
// }
};
const proxy = new Proxy(target, handler);
console.log(proxy.foo); // bar
console.log(target.foo); // bar
在此基础上开发者可以用最少的代码修改捕获的方法:
const handler = {
get(trapTarget, property, receiver) {
let decoration = '';
if (property === 'foo') {
decoration = '!!!';
}
return Reflect.get(...arguments) + decoration;
}
};
1.4 捕获器不变式
比如,如果目标对象有一个不可配置且不可写的数据属性,那么在捕获器返回一个与该属性不同的值时,会抛出 TypeError:
const target = {};
Object.defineProperty(target, 'foo', {
configurable: false,
writable: false,
value: 'bar'
});
const handler = {
get() {
return 'qux';
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.foo);
// TypeError
1.5 可撤销代理
Proxy 也暴露了 revocable()方法,这个方法支持撤销代理对象与目标对象的关联。撤销代理的操作是不可逆的。撤销代理之后再调用代理会抛出 TypeError。
撤销函数和代理对象是在实例化时同时生成的:
const target = {
foo: 'bar'
};
const handler = {
get() {
return 'intercepted';
}
};
const { proxy, revoke } = Proxy.revocable(target, handler);
console.log(proxy.foo); // intercepted
console.log(target.foo); // bar
revoke();
console.log(proxy.foo); // TypeError
1.6 实用反射 API
1. 反射 API 与对象 API
(1) 反射 API 并不限于捕获处理程序;
(2) 大多数反射 API 方法在 Object 类型上有对应的方法。
通常,Object 上的方法适用于通用程序,而反射方法适用于细粒度的对象控制与操作。
2.状态标记
很多反射方法返回称作“状态标记”的布尔值,表示意图执行的操作是否成功。有时候,状态标记比那些返回修改后的对象或者抛出错误(取决于方法)的反射 API 方法更有用。例如,可以使用反射API 对下面的代码进行重构:
// 在定义新属性时如果发生问题,Reflect.defineProperty()会返回 false,而不是抛出错误。
const o = {};
if(Reflect.defineProperty(o, 'foo', {value: 'bar'})) {
console.log('success');
} else {
console.log('failure');
}
以下反射方法都会提供状态标记:
Reflect.defineProperty()
Reflect.preventExtensions()
Reflect.setPrototypeOf()
Reflect.set()
Reflect.deleteProperty()
3. 用一等函数替代操作符
以下反射方法提供只有通过操作符才能完成的操作。
Reflect.get():可以替代对象属性访问操作符。
Reflect.set():可以替代=赋值操作符。
Reflect.has():可以替代 in 操作符或 with()。
Reflect.deleteProperty():可以替代 delete 操作符。
Reflect.construct():可以替代 new 操作符。
4.安全地应用函数
在通过 apply 方法调用函数时,被调用的函数可能也定义了自己的 apply 属性(虽然可能性极小)。为绕过这个问题,可以使用定义在 Function 原型上的 apply 方法,比如:
Function.prototype.apply.call(myFunc, thisVal, argumentList);
这种可怕的代码完全可以使用 Reflect.apply 来避免:
Reflect.apply(myFunc, thisVal, argumentsList);
1.7 代理另一个代理
const target = {
foo: 'bar'
};
const firstProxy = new Proxy(target, {
get() {
console.log('first proxy');
return Reflect.get(...arguments);
}
});
const secondProxy = new Proxy(firstProxy, {
get() {
console.log('second proxy');
return Reflect.get(...arguments);
}
});
console.log(secondProxy.foo);
// second proxy
// first proxy
// bar
1.8 代理的问题与不足
1.代理中的 this
const wm = new WeakMap();
class User {
constructor(userId) {
wm.set(this, userId);
}
set id(userId) {
wm.set(this, userId);
}
get id() {
return wm.get(this);
}
}
由于这个实现依赖 User 实例的对象标识,在这个实例被代理的情况下就会出问题:
const user = new User(123);
console.log(user.id); // 123
const userInstanceProxy = new Proxy(user, {});
console.log(userInstanceProxy.id); // undefined
// 这是因为 User 实例一开始使用目标对象作为 WeakMap 的键,代理对象却尝试从自身取得这个实
//例。
解决方法,把代理 User 实例改为代理 User 类本身。之后再创建代
理的实例就会以代理实例作为 WeakMap 的键了:
const UserClassProxy = new Proxy(User, {});
const proxyUser = new UserClassProxy(456);
console.log(proxyUser.id);
2.代理与内部槽位
Date 类型方法的执行依赖 this 值上的内部槽位[[NumberDate]]。代理对象上不存在这个内部槽位,于是代理拦截后本应转发给目标对象的方法会抛出 TypeError:
const target = new Date();
const proxy = new Proxy(target, {});
console.log(proxy instanceof Date); // true
proxy.getDate(); // TypeError: 'this' is not a Date object
2 代理捕获器与反射方法
2.1 get()
get()捕获器会在获取属性值的操作中被调用。对应的反射 API 方法为 Reflect.get()。
const myTarget = {};
const proxy = new Proxy(myTarget, {
get(target, property, receiver) {
console.log('get()');
return Reflect.get(...arguments)
}
});
proxy.foo;
// get()
1. 返回值
返回值无限制。
2. 拦截的操作
proxy.property
proxy[property]
Object.create(proxy)[property]
Reflect.get(proxy, property, receiver)
3. 捕获器处理程序参数
target:目标对象。
property:引用的目标对象上的字符串键属性。
receiver:代理对象或继承代理对象的对象。
4. 捕获器不变式
如果 target.property 不可写且不可配置,则处理程序返回的值必须与 target.property 匹配。如果 target.property 不可配置且[[Get]]特性为 undefined,处理程序的返回值也必须是 undefined。
2.2 set()
对应的反射 API 方法为 Reflect.set()。
const proxy = new Proxy(myTarget, {
set(target, property, value, receiver) {
console.log('set()');
return Reflect.set(...arguments)
}
});
1. 返回值
返回 true 表示成功;返回 false 表示失败,严格模式下会抛出 TypeError
2. 拦截的操作
proxy.property = value
proxy[property] = value
Object.create(proxy)[property] = value
Reflect.set(proxy, property, value, receiver)
3. 捕获器处理程序参数
target:目标对象。
property:引用的目标对象上的字符串键属性。
value:要赋给属性的值。
receiver:接收最初赋值的对象。
4. 捕获器不变式
如果 target.property 不可写且不可配置,则不能修改目标属性的值。如果 target.property 不可配置且[[Set]]特性为 undefined,则不能修改目标属性的值。在严格模式下,处理程序中返回 false 会抛出 TypeError。
2.3 has()
has()捕获器会在 in 操作符中被调用。
const myTarget = {};
const proxy = new Proxy(myTarget, {
has(target, property) {
console.log('has()');
return Reflect.has(...arguments) // 必须返回布尔值
}
});
'foo' in proxy; // 拦截的操作: in、with(proxy) {(property);}
// has()
2.4 defineProperty()
const myTarget = {};
const proxy = new Proxy(myTarget, {
defineProperty(target, property, descriptor) {
console.log('defineProperty()');
return Reflect.defineProperty(...arguments) // 返回布尔值,表示属性是否成功定义。会被转换为布尔值
}
});
// 拦截Object.defineProperty、Reflect.defineProperty
Object.defineProperty(proxy, 'foo', { value: 'bar' });
// defineProperty()
2.5 apply()
apply()捕获器会在调用函数时中被调用。对应的反射 API 方法为 Reflect.apply()。
const myTarget = () => {};
const proxy = new Proxy(myTarget, {
apply(target, thisArg, ...argumentsList) {
console.log('apply()');
return Reflect.apply(...arguments)
}
});
proxy();
// apply()
返回值无限制,拦截的操作:
proxy(…argumentsList)
Function.prototype.apply(thisArg, argumentsList)
Function.prototype.call(thisArg, …argumentsList)
Reflect.apply(target, thisArgument, argumentsList)
参数:
target:目标对象
thisArg:调用函数时的 this 参数。
argumentsList:调用函数时的参数列表
2.6 其他的
- getOwnPropertyDescriptor()
- deleteProperty()
- ownKeys()
- getPrototypeOf()
- setPrototypeOf()
- isExtensible()
- preventExtensions()
- construct()
3 代理模式
3.1 跟踪属性访问
通过捕获 get、set 和 has 等操作,可以知道对象属性什么时候被访问、被查询。
3.2 隐藏属性
代理的内部实现对外部代码是不可见的,可以隐藏目标对象上的属性:
const hiddenProperties = ['foo', 'bar'];
const targetObject = {
foo: 1,
bar: 2,
baz: 3
};
const proxy = new Proxy(targetObject, {
get(target, property) {
if (hiddenProperties.includes(property)) {
return undefined;
} else {
return Reflect.get(...arguments);
}
},
has(target, property) {
if (hiddenProperties.includes(property)) {
return false;
} else {
return Reflect.has(...arguments);
}
}
});
// get()
console.log(proxy.foo); // undefined
console.log(proxy.bar); // undefined
console.log(proxy.baz); // 3
// has()
console.log('foo' in proxy); // false
console.log('bar' in proxy); // false
console.log('baz' in proxy); // true
3.3 属性验证
因为所有赋值操作都会触发 set()捕获器,所以可以根据所赋的值决定是允许还是拒绝赋值:
const target = {
onlyNumbersGoHere: 0
};
const proxy = new Proxy(target, {
set(target, property, value) {
if (typeof value !== 'number') {
return false;
} else {
return Reflect.set(...arguments);
}
}
});
proxy.onlyNumbersGoHere = 1;
console.log(proxy.onlyNumbersGoHere); // 1
proxy.onlyNumbersGoHere = '2';
console.log(proxy.onlyNumbersGoHere); // 1
3.4 函数与构造函数参数验证
可对函数和构造函数参数进行审查。比如,可以让函数只接收某种
类型的值:
function median(...nums) {
return nums.sort()[Math.floor(nums.length / 2)];
}
const proxy = new Proxy(median, {
apply(target, thisArg, argumentsList) {
for (const arg of argumentsList) {
if (typeof arg !== 'number') {
throw 'Non-number argument provided';
}
}
return Reflect.apply(...arguments);
}
});
console.log(proxy(4, 7, 1)); // 4
console.log(proxy(4, '7', 1));
// Error: Non-number argument provided
4 小结
代理是 ECMAScript 6 新增的令人兴奋和动态十足的新特性。尽管不支持向后兼容,但它开辟出了一片前所未有的 JavaScript 元编程及抽象的新天地。
从宏观上看,代理是真实 JavaScript 对象的透明抽象层。代理可以定义包含捕获器的处理程序对象,而这些捕获器可以拦截绝大部分 JavaScript 的基本操作和方法。在这个捕获器处理程序中,可以修改任何基本操作的行为,当然前提是遵从捕获器不变式。
与代理如影随形的反射 API,则封装了一整套与捕获器拦截的操作相对应的方法。可以把反射 API看作一套基本操作,这些操作是绝大部分 JavaScript 对象 API 的基础。
代理的应用场景是不可限量的。开发者使用它可以创建出各种编码模式,比如(但远远不限于)跟踪属性访问、隐藏属性、阻止修改或删除属性、函数参数验证、构造函数参数验证、数据绑定,以及可观察对象。