1. 基础
Proxy对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
本质: 修改的是程序默认行为,相当于在编程语言层面上做修改,属于元编程(Meta Programming)。
1.1. 用法
const p = new Proxy(target, handler)
target:要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)。
handler: 一个通常以函数作为属性的对象,函数可以自定义在执行操作时对应的行为。
const obj = {}
const handler = {
get(target, property, receiver) {
// do something
return Reflect.get(...arguments)
}
}
const p = new Proxy(obj, handler)
console.log(p.a)
使用代理的主要目的是可以定义捕获器(trap)。
- 捕获器就是在处理程序对象中定义的“基本操作的拦截器”。
- 每个处理程序对象可以包含零个或多个捕获器,每个捕获器都对应一种基本操作。
- 每次在代理对象上调用这些基本操作时,代理可以在这些操作传播到目标对象之前,
先调用捕获器函数,从而拦截并修改相应的行为。 - 只有
在代理对象上执行这些操作才会触发捕获器。在目标对象上执行这些操作仍然会产生正常的行为。
const dinner = {
meal: 'tacos'
}
const handler = {
get(target, property) {
console.log('intercepted!')
return target[property]
}
}
const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)
// intercepted! (拦截)
// tacos
const handler = {
get: function(obj, prop) {
return prop in obj ? obj[prop] : 37;
}
};
const obj = { a: 'a' }
const p = new Proxy(obj, handler);
p.a = 1;
p.b = 'b';
p.c = undefined
console.log(obj)
console.log(p.a, p.b, p.c);
console.log('d' in p, p.d);
console.log('d' in obj, obj.d)
console.log(obj == p)
// { a: 1, b: 'b', c: undefined }
// 1, b, undefined
// false, 37
// false, undefined
// false
1.2. 捕获器介绍
捕获器(trap)是从操作系统中借用的概念。在操作系统中,捕获器是程序流中的一个同步中断,可以暂停程序流,转而执行一段子例程,之后再返回原始程序流。
13个Trap,与Reflect的13个方法一一对应。Trap可以看做是相应操作的钩子。
Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。
- get(target, propKey, receiver):拦截读取对象属性的操作,返回读取到的值
- 访问属性: proxy[foo] 和 proxy.bar
- 访问原型链上的属性: Object.create(proxy)[foo]
- Reflect.get()
- set(target,propKey,value,receiver):拦截对象属性的设置操作,返回应当返回一个
布尔值,设置成功返回true
- 指定属性值: proxy[foo] = bar 和 proxy.foo = bar
- 指定继承者的属性值: Object.create(proxy)[foo] = bar
- Reflect.set()
- has(target,propKey):拦截propKey in proxy等查询操作,返回一个
布尔值
- 属性查询: foo in proxy
- 继承属性查询: foo in Object.create(proxy)
with检查 : with(proxy) { (foo); }- Reflect.has()
- deleteProperty(target,propKey):拦截delete proxy[propKey]的操作,返回一个
布尔值
- 删除属性: delete proxy[foo] 和 delete proxy.foo*
- Reflect.deleteProperty()
- ownKeys(target):拦截Object.keys(proxy)、for...in等遍历操作,返回一个
数组
- Object.getOwnPropertyNames()
- Object.getOwnPropertySymbols()
- Object.keys()与for...in
- Reflect.ownKeys()
6. getOwnPropertyDescriptor => Object.getOwnPropertyDescriptor()
7. defineProperty => Object.defineProperty()
8. preventExtensions => Object.preventExtensions()
9. getPrototypeOf => Object.getPrototypeOf()
10. isExtensible => Object.isExtensible()
11. setPrototypeOf => Object.setPrototypeOf()
12. apply(target, object, args):拦截 Proxy 实例作为函数调用的操作
13. construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,new
1.3 可撤销代理
中断代理对象与目标对象之间的联系,该撤销操作不可逆。
使用 Proxy.revocable()
const target = { name: 'vuejs'}
const {proxy, revoke} = Proxy.revocable(target, {})
console.log(proxy.name) // 正常取值输出 vuejs
revoke() // 取值完成对proxy进行封闭,撤消代理
proxy.name = 3 // TypeError: Cannot perform 'set' on a proxy that has been revoked
2. 编程模式
2.1 隐藏属性
通过拦截get、set,来防止外部获取、修改对象内部的某些属性。
const targetObject = {
age: 18,
_age: 32,
state: 'fighting',
_state: 'loafing'
};
const proxy = new Proxy(targetObject, {
get(target, property) {
if (property.startsWith('_')) {
return undefined;
} else {
return Reflect.get(...arguments);
}
},
has(target, property) {
if (property.startsWith('_')) {
return false;
} else {
return Reflect.has(...arguments);
}
}
});
// get()
console.log(proxy._state); // undefined
console.log(proxy.state); // 'loafing'
// has()
console.log('_state' in proxy); // false
console.log('state' in proxy); // true
2.2 属性校验
set时,根据所赋的值决定是否允许赋值
下面的代码实现只允许为属性赋予number类型的值
const target = {
onlyNumbers: 0
}
const proxy = new Proxy(target, {
set(target, property, value) {
if(typeof value !== 'number') {
return false;
} else {
return Reflect.set(...arguments)
}
}
})
proxy.onlyNumbers = 1
console.log(proxy.onlyNumbers) // 1
proxy.onlyNumbers = '22'
console.log(proxy.onlyNumbers) // 1
2.3 观察者模式
const queuedObservers = new Set();
const observe = fn => queuedObservers.add(fn);
const observable = obj => new Proxy(obj, {
set(target, key, value, receiver) {
if(value === target[key]) return;
const result = Reflect.set(target, key, value, receiver);
// 遍历调用集合中的函数
queuedObservers.forEach(observer => observer());
return result;
}
});
const person = observable({
name: '张三',
age: 20
});
const watchPerson = () => {
console.log(`修改后的人物信息:${person.name}, ${person.age}`)
}
observe(watchPerson); // 将函数添加入集合
// 会触发set拦截器
person.name = '李四';
person.age = 19;
// 修改后的人物信息:李四, 20
// 修改后的人物信息:李四, 19
3. 其他
3.1 Trap Invariant
每个Trap都可以intercept特定操作。属性若是不变式(Invariant),当该Trap改变默认行为时,抛出TypeError。
Trap Get的约束
- 如果要访问的目标属性是不可写以及不可配置的,则返回的值必须与该目标属性的值相同。
- 如果要访问的目标属性没有配置访问方法,即get方法是undefined的,则返回值必须为undefined。
var obj = {};
Object.defineProperty(obj, "a", {
configurable: false,
enumerable: false,
value: 10,
writable: false
});
var p = new Proxy(obj, {
get: function(target, prop) {
return 20;
}
});
p.a; //会抛出TypeError
// 'get' on proxy: property 'a' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected 'undefined' but got '20')