Proxy简介
Proxy用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。
Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
ES6 原生提供 Proxy
构造函数,用来生成 实例
var proxy = new Proxy(target, handler);
//其中 target表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为
实例
var obj = new Proxy({}, {
get: function (target, key, receiver) {
console.log(`getting ${key}!`);
return Reflect.get(target, key, receiver);
},
set: function (target, key, value, receiver) {
console.log(`setting ${key}!`);
return Reflect.set(target, key, value, receiver);
}
});
obj.count = 1
// setting count!
++obj.count
// getting count!
// setting count!
// 2
上述代码对一个空对象假设了一层拦截,重新定义了属性的get
和set
操作,返回一个Proxy
,其中参数target
为拦截对象,key
为接受到的键名,value
为接受到的键值,receiver
为一个可选的对象。因此,当对返回的Proxy
读取和修改属性值时,就会调用相应的自定义方法,在控制台输出字符串。
同一个hander
对象内可以设置多个拦截操作,对于没有设置的操作,遵从原先的方式产生结果。注意,要使得Proxy起作用,必须针对Proxy实例进行操作,而不是针对目标对象进行操作,否则定义的拦截操作不会起作用。
Proxy支持的拦截操作包括
(1)get(target, propKey, receiver)
:拦截对象属性的读取,比如proxy.foo
和proxy['foo']
var person = {
name: "张三"
};
var proxy = new Proxy(person, {
get: function(target, property) {
if (property in target) {
return target[property];
} else {
throw new ReferenceError("Property \"" + property + "\" does not exist.");
}
}
});
proxy.name // "张三"
proxy.age // 抛出一个错误
上面代码表示,如果访问目标对象不存在的属性,会抛出一个错误。如果没有这个拦截函数,访问不存在的属性,只会返回undefined
。
get
方法可以继承
let proto = new Proxy({}, {
get(target, propertyKey, receiver) {
console.log('GET ' + propertyKey);
return target[propertyKey];
}
});
let obj = Object.create(proto);
obj.foo // "GET foo"
第三个参数receiver
表示原始操作所在的对象
const proxy = new Proxy({}, {
get: function(target, property, receiver) {
return receiver;
}
});
proxy.getReceiver === proxy // true 原始读操作就在proxy对象
const d = Object.create(proxy);
d.a === d // true 原始读操作在d对象
如果一个属性不可配置(configurable)且不可写(writable),则 Proxy 不能修改该属性,否则通过 Proxy 对象访问该属性会报错
(2) set(target, propKey, value, receiver)
:拦截对象属性的设置,比如proxy.foo = v
或proxy['foo'] = v
,返回一个布尔值。
结合get
和set
方法,就可以做到防止某些以下划线(_
)开头,表示内部的属性被外部读写。
const handler = {
get (target, key) {
invariant(key, 'get');
return target[key];
},
set (target, key, value) {
invariant(key, 'set');
target[key] = value;
return true;
}
};
function invariant (key, action) {
if (key[0] === '_') {
throw new Error(`Invalid attempt to ${action} private "${key}" property`);
}
}
const target = {};
const proxy = new Proxy(target, handler);
proxy._prop
// Error: Invalid attempt to get private "_prop" property
proxy._prop = 'c'
// Error: Invalid attempt to set private "_prop" property
上述代码,只要读写属性的第一个字符是下划线,则一律报错,达到禁止读写内部属性的目的
同样与get
操作类似,set
操作的receiver
参数代表原始操作行为所在的那个对象
const handler = {
set: function(obj, prop, value, receiver) {
obj[prop] = receiver;
}
};
const proxy = new Proxy({}, handler);
proxy.foo = 'bar';
proxy.foo === proxy // true
const myObj = {};
Object.setPrototypeOf(myObj, proxy);
myObj.foo = 'bar';
myObj.foo === myObj // true
注意,如果目标对象自身的某个属性,不可写且不可配置,那么set
方法将不起作用。
(3)apply(target, object, args)
:拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)
、proxy.call(object, ...args)
、proxy.apply(...)
。apply方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组。
var target = function () { return 'I am the target'; };
var handler = {
apply: function () {
return 'I am the proxy';
}
};
var p = new Proxy(target, handler);
p()
// "I am the proxy"
上面代码中,变量p
是 Proxy 的实例,当它作为函数调用时(p()
),就会被apply
方法拦截,返回一个字符串。
另外,直接调用Reflect.apply
方法,也会被拦截。
Reflect.apply(p, null, []) // 'I am the proxy'
(4)has(target, propKey)
:拦截propKey in proxy
的操作,返回一个布尔值。判断对象是否具有某个属性时,这个方法会生效。典型的操作就是in
运算符。
var handler = {
has (target, key) {
if (key[0] === '_') {
return false;
}
return key in target;
}
};
var target = { _prop: 'foo', prop: 'foo' };
var proxy = new Proxy(target, handler);
'_prop' in proxy // false
上面代码中,如果原对象的属性名的第一个字符是下划线,proxy.has
就会返回false
,从而不会被in
运算符发现。
如果原对象不可配置或者禁止扩展,这时has
拦截会报错。has方法拦截的是HasProperty
操作,而不是HasOwnProperty
操作,即has
方法不判断一个属性是对象自身的属性,还是继承的属性。虽然for…in循环也用到了in运算符,但是has拦截对for…in循环不生效。
(5)construct(target,args)
:拦截Proxy
实例作为构造函数调用的操作,比如new proxy(...args)
。返回的必须是一个对象,否则会报错
construct方法可以接受两个参数:
- target:目标对象
- args:构造函数的参数对象
var p = new Proxy(function () {}, {
construct: function(target, args) {
console.log('called: ' + args.join(', '));
return { value: args[0] * 10 };
}
});
(new p(1)).value
// "called: 1"
// 10
(6)deleteProperty(target, propKey)
:拦截delete proxy[propKey]
的操作,返回一个布尔值。如果这个方法抛出错误或者返回false
,当前属性就无法被delete
命令删除。
var handler = {
deleteProperty (target, key) {
invariant(key, 'delete');
return true;
}
};
function invariant (key, action) {
if (key[0] === '_') {
throw new Error(`Invalid attempt to ${action} private "${key}" property`);
}
}
var target = { _prop: 'foo' };
var proxy = new Proxy(target, handler);
delete proxy._prop
// Error: Invalid attempt to delete private "_prop" property
目标对象自身的不可配置(configurable)的属性,不能被deleteProperty方法删除,否则报错。
(7)defineProperty(target,propKey,propDesc)
:拦截Object.defineProperty()
、Object.defineProperties()
,返回一个布尔值。如果返回false
,则导致新添加的属性总是无效
var handler = {
defineProperty (target, key, descriptor) {
return false;
}
};
var target = {};
var proxy = new Proxy(target, handler);
proxy.foo = 'bar' // 不会生效
注意,如果目标对象不可扩展(extensible),则defineProperty
不能增加目标对象上不存在的属性,否则会报错。另外,如果目标对象的某个属性不可写(writable)或不可配置(configurable),则defineProperty
方法不得改变这两个设置。
(8)getOwnPropertyDescriptor(target, propKey)
:拦截Object.getOwnPropertyDescriptor(proxy, propKey)
,返回属性的描述对象或undefined
。
(9)getPrototypeOf(target)
:拦截Object.getPrototypeOf(proxy)
,返回一个对象。返回值必须是对象或null,另外如果对象不可扩展,则必须返回目标对象的原型对象
(10)isExtensible(target)
:拦截Object.isExtensible(proxy)
,返回一个布尔值。该方法有个限制,其返回值必须跟对象的isExtensible
属性保持一致,也就是如果对象可以扩展,你必须返回true
(11)preventExtensions(target)
:拦截Object.preventExtensions(proxy)
,返回一个布尔值。该方法有个限制,只有当目标对象不可扩展时,才能返回true,否则会报错。
var p = new Proxy({}, {
preventExtensions: function(target) {
return true;
}
});
Object.preventExtensions(p) // 报错
为了防止出现这个问题,通常要在proxy.preventExtensions
方法里面,调用一次Object.preventExtensions
。
(12)ownKeys(target)
:拦截Object.getOwnPropertyNames(proxy)
、Object.getOwnPropertySymbols(proxy)
、Object.keys(proxy)
、for...in
循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()
的返回结果仅包括目标对象自身的可遍历属性。
下面的例子是拦截第一个字符为下划线的属性名不被遍历。
let target = {
_bar: 'foo',
_prop: 'bar',
prop: 'baz'
};
let handler = {
ownKeys (target) {
return Reflect.ownKeys(target).filter(key => key[0] !== '_');
}
};
let proxy = new Proxy(target, handler);
for (let key of Object.keys(proxy)) {
console.log(target[key]);
}
// "baz"
ownKeys
方法返回的数组成员,只能是字符串或 Symbol 值。如果有其他类型的值,或者返回的根本不是数组,就会报错。
如果目标对象自身包含不可配置的属性,则该属性必须被ownKeys方法返回,否则报错。如果目标对象是不可扩展的(non-extensition),这时ownKeys方法返回的数组之中,必须包含原对象的所有属性,且不能包含多余的属性,否则报错。
(13)setPrototypeOf(target, proto)
:拦截Object.setPrototypeOf(proxy, proto)
,返回一个布尔值。
如果目标对象不可扩展(extensible),setPrototypeOf
方法不得改变目标对象的原型。
Proxy.revocable()
该方法返回一个可取消的Proxy实例和一个取消函数,调用该函数,可以取消Proxy实例,不再允许访问
let target = {};
let handler = {};
let {proxy, revoke} = Proxy.revocable(target, handler);
proxy.foo = 123;
proxy.foo // 123
revoke();
proxy.foo // TypeError: Revoked
Proxy.revocable
的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。
this问题
在 Proxy 代理的情况下,目标对象内部的this
关键字会指向 Proxy 代理。将this
绑定原始对象可以解决这个问题
const target = new Date();
const handler = {};
const proxy = new Proxy(target, handler);
proxy.getDate(); //有些原生对象的内部属性,只有通过正确的this才能拿到
// TypeError: this is not a Date object.
const target = new Date('2015-01-01');
const handler = {
get(target, prop) {
if (prop === 'getDate') {
return target.getDate.bind(target); //绑定this至目标对象
}
return Reflect.get(target, prop);
}
};
const proxy = new Proxy(target, handler);
proxy.getDate() // 1