Proxy
概述
用于修改某些操作默认行为,等同于在语言层面做出修改,元编程。实际上Proxy重载了点运算符
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.get(target,key,value,receiver);
}
})
没有拦截直接通向原对象
一个技巧是将Proxy设置到object.proxy
var object={proxy: new Proxy(target, handler)};
Proxy实例也作为其他对象原型对象
var proxy = new Proxy({},{
get:function(target,property){
return 35;
}
});
let obj=Object.create(proxy);
obj.time //35
上面代码中。proxy是obj对象原型,obj对象本身没有time属性,所以根据原型链,会在proxy对象读取属性,导致被拦截。
共支持13中拦截操作。
get
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.")
}
}
})
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"
get拦截,实现数组读取负数的索引
function createArray(...elements){
let handler={
get(target,propKey,receiver){
let index=Number(propKey);
if(index < 0){
propKey=String(target.length + index);
}
return Reflect.get(target,propKey,receiver);
}
};
let target=[];
target.push(...elements);
return new Proxy(target, handler);
}
let arr = createArray('a','b','c');
a[-1]//c
利用Proxy可将读取属性操作get,转变为执行某个函数,实现属性链式操作。
var pipe=(function(){
return function(value){
var funcStack=[];
var oproxy=new Proxy({},{
get:function(pipeObject,fnName){
if(fnName === 'get'){
return funcStack.reduce(function(val,fn){
return fn(val);
},value);
}
funcStack.push(window[fnName]);
return oproxy;
}
});
return oproxy;
}
}());
var double = n => n*2;
var pow = n => n * n;
var reverseInt = n => n.toString().split("").reverse().join("")|0;
pip(3).double.pow.reverseInt.get;//63
上面代码达到函数名链式调用效果。
下面例子利用get拦截,实现一个生成各种dom节点通用dom。
const dom=new Proxy({},{
get(target,property){
return function(attrs={},...children){
const el=document.createElement(property);
for(let prop of Object.keys(attrs){
el.setAttribute(prop,attrs[prop]);
}
for(let child of children){
if(typeof child === 'string'){
child = document.createTextNode(child);
}
el.appendChild(child);
}
return el;
}
}
});
const el=dom.div({},
"Hello, my name is ",
dom.a({href:'//example.com'},'Mark'),
'. I like:',
dom.ul({},
dom.li({},"The web"),
dom.li({},"Food"),
dom.li({},"...actually that\'s it")
)
);
document.body.appendChild(el);
下面是一个get方法第三个参数的例子,总是指向原始的读操作所在的那个对象,一般就是proxy
const proxy = new Proxy({},{
get: function(target,property,receiver){
return [target,receiver];
}
});
const d=Object.create(proxy);
d.a===d
如果一个属性不可配置不可写,会报错
const target=Object.defineProperties({},{
foo:{
value:123,
writable:false,
configurable:false
},
});
const handler={
get(target,propKey){
return 'abc';
}
};
const proxy=new Proxy(target, handler);
proxy.foo//error
set
拦截赋值操作,四个参数目标,属性,属性值,proxy实例
假定person有age属性,该属性不大于200整数
let validator={
set:function(obj, prop, value){
if(prop === 'age'){
if(!Number.isInteger(value)){
throw new TypeError('The age is not an integer');
}
if(value > 200){
throw new RangeError('The age seems invalid');
}
}
//对于满足条件age属性以及其他属性,直接保存
obj[prop]=value;
}
};
let person=new Proxy({}, validator);
person.age=100;
person.age//100
person.age='young'//报错
person.age=300//error
设置内部属性,防止外部读写
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('');
}
}
const target={};
const proxy = new Proxy(target,handler);
proxy._prop
set的第四个参数,receiver指的是原始操作行为所在的那个对象,一般情况下是proxy实例本身
const handler={
set:function(obj,prop,value,receiver){
obj[prop]=receiver;
}
};
const proxy=new Proxy({},handler);
const myObj={};
Object.setPrototypeOf(myObj,proxy);
myObj.foo='bar';
myObj.foo===muObj
会到muObj原型链去找foo,这时receiver就指向原始赋值行为所在的对象myObj
apply
拦截函数调用、call、apply,三个参数目标对象,目标对象的上下文和目标对象参数数组。
var hanler={
apply(target, ctx, args){
return Reflect.apply(...arguments);
}
}
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
p是proxy实例,作为函数掉用时(p()),被apply方法拦截
var twice={
apply(target, ctx, args){
return Reflect.apply(...arguments)*2;
}
};
function sum(left,right){
return left + right;
}
var proxy=new Proxy(sum, twice);
proxy(1,2)//6
proxy.call(null,5,6)//22
proxy.apply(null,[7,8])//30
每当执行proxy函数或者调用call,apply,就会被拦截
直接调用Reflect.apply方法,也会被拦截
Reflect.apply(proxy,null,[9,10]);//30
has()
has拦截HasProperty操作,即判断对象是否具有某个属性,in,
两个参数,目标对象,需查询的属性名
下面利用has方法隐藏某些属性,不被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
如果原对象不可配置或者进制扩展,has拦截会报错。
var obj={ a:10 };
Object.preventExtensions(obj);
var p = new Proxy(obj, {
has: function(target, prop){
return false;
}
})
'a' in p//typeError
如果某个属性不可配置或目标对象不可扩展,has方法就不得隐藏
has拦截的是HasProperty操作,而不是HasOwnProperty操作,即has不判断一个属性是自身属性还是继承属性
对for … in 不生效
construct()
用于拦截new
var handler={
construct (targetm,args,newTarget){
return new target(...args);
}
};
接受两个参数,target目标对象,args构造函数参数对象,newTarget创造实例对象时,new命令作用的构造函数。
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
必须返回对象,否则报错
deleteProperty()
拦截delete,这个方法抛出错误或者return false,当前属性就无法被delete
var handler={
deleteProperty(target, key){
invariant(key, 'delete');
delete target[key];
return true;
}
}
function invariant(key, action){
if(key[0] === '_'){
throw new Error(`Invalid attempt to `);
}
}
var target={_prop: 'foo'};
var proxy._prop
//Error
目标对象自身的不可配置configurable属性,不能删除,否则报错
defineProperty()
拦截Object.defineProperty操作
var handler={
defineProperty(target,key,descriptor){
return false;
}
}
var target={}
var proxy=new Proxy(target, handler);
proxy.foo='bar'//不会生效
如果目标对象不可扩展,define不能增加目标对象不存在的属性,否则会报错,如果目标对象某个属性不可写不可配置,不得改变这两个设置
getOwnPropertyDescriptor
getOwnPropertyDescriptor方法拦截Object.getOwnPropertyDescriptor() 返回一个属性描述对象或者undefined。
var handler={
getOwnPropertyDescriptor(target,key){
if(key[0] === '_'){
return ;
}
return Object.getOwnPropertyDescriptor(target,key)
}
};
var target = { _foo: 'bar', baz: 'tar' };
var proxy = new Proxy(target, handler);
Object.getOwnPropertyDescriptor(proxy, 'wat')
// undefined
Object.getOwnPropertyDescriptor(proxy, '_foo')
// undefined
Object.getOwnPropertyDescriptor(proxy, 'baz')
// { value: 'tar', writable: true, enumerable: true, configurable: true }
getPrototypeOf()
拦截获取对象原型
Object.prototype.proto
Object.prototype.isPrototypeOf()
Object.getPrototypeOf()
Reflect.getPrototypeOf()
instanceof
例子
var ptoto={};
var p = new Proxy({},{
getPrototypeOf(target){
return proto;
}
});
Object.getPrototypeOf(p) === proto
必须返回nul或者对象,不可扩展,必须返回目标原型
isExtensible()
拦截Object.isExtensible操作
var p=new Proxy({},{
isExtensible: function(target){
console.log('called');
return true;
}
});
Object.isExtensible(p);
//called true
只返回布尔值,必须与目标对象isExtensible属性保持一致
ownKeys()
ownKeys拦截以下操作
Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Object.keys()
for…in循环
let target={
a:1,
b:2,
c:3
};
let handler={
ownKeys(target){
return ['a'];
}
};
let proxy=new Proxy(target, handler);
Object.keys(proxy);
//["a"]
拦截了target的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]);
}
三类属性会被ownKeys自动过滤
目标对象不存在的属性,属性名为Symbol属性,不可遍历enumerable属性
let target = {
a: 1,
b: 2,
c: 3,
[Symbol.for('secret')]: '4',
};
Object.defineProperty(target, 'key', {
enumerable: false,
configurable: true,
writable: true,
value: 'static'
});
let handler = {
ownKeys(target) {
return ['a', 'd', Symbol.for('secret'), 'key'];
}
};
let proxy = new Proxy(target, handler);
Object.keys(proxy)
// ['a']
ownKeys拦截Object.getOwnPropertyNames()
var p=new Proxy({},{
ownKeys:function(target){
return ['a','b','c'];
}
});
Object.getOwnPropertyNames(p)
//['a','b','c']
for…in循环也受到ownKeys方法的拦截
const obj={ hello: 'world' };
const proxy = new Proxy(obj,{
ownKeys:function(){
return ['a', 'b'];
}
});
for(let key in proxy){
console.log(key)
}
ownkeys只返回a和b属性,由于obj没有这两个属性,因此不会有输出
ownKeys方法返回的数组成员,如果有其他类型的值或者不是数组,就会报错
var obj={};
var p=new Proxy(obj, {
ownKeys: function(target){
return [123,true,undefined,null,{},[]];
}
});
Object.getOwnPropertyNames(p);
每一个数组成员不是字符串或者Symbol值。
如果该目标对象自身包含不可配置属性,必须被返回,否则报错。
var obj = {};
Object.defineProperty(obj, 'a', {
configurable: false,
enumerable: true,
value: 10 }
);
var p = new Proxy(obj, {
ownKeys: function(target) {
return ['b'];
}
});
Object.getOwnPropertyNames(p)
// Uncaught TypeError: 'ownKeys' on proxy: trap result did not include 'a'
如果目标对象是不可扩展的,这时候ownKeys方法返回数组之中,必须包含原对象所有属性,不能包含多余属性,否则报错。
var obj = {
a: 1
};
Object.preventExtensions(obj);
var p = new Proxy(obj, {
ownKeys: function(target) {
return ['a', 'b'];
}
});
Object.getOwnPropertyNames(p)
// Uncaught TypeError: 'ownKeys' on proxy: trap returned extra keys but proxy target is non-extensible
preventExtensions()
拦截Object.preventExtensions()
必须返回一个布尔值,否则会被自动转为布尔值。
有一个限制,只有目标对象不可扩展Object.isExtensible(proxy)为false,proxy.preventExtensions返回true
var proxy = new Proxy({}, {
preventExtensions: function(target) {
return true;
}
});
Object.preventExtensions(proxy)
// Uncaught TypeError: 'preventExtensions' on proxy: tr
为了防止出现这个问题,要在proxy.preventExtensions方法里调用Object.preventExtensions
var proxy = new Proxy({}, {
preventExtensions: function(target) {
console.log('called');
Object.preventExtensions(target);
return true;
}
});
Object.preventExtensions(proxy)
// "called"
// Proxy {}
setPrototypeOf()
用来拦截Obejct.setPrototypeOf方法
下面的一个例子
var handler={
setPrototypeOf(target,proto){
throw new Error('Changeing the prototype is forbidden');
}
};
var proto={};
var target = function(){};
var proxy=new Proxy(target, handler);
Object.setPrototypeOf(proxy, proto);
// Error: Changing the prototype is forbidden
上面代码中,只要修改target的原型对象,就会报错。
注意,该方法只能返回布尔值,否则会被自动转为布尔值。另外,如果目标对象不可扩展(non-extensible),setPrototypeOf方法不得改变目标对象的原型。
Proxy.revocable()
返回一个可取消的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代理针对目标对象访问,proxy代理情况下,目标对象内部的this关键字会指向 Proxy 代理。
const target = {
m: function () {
console.log(this === proxy);
}
};
const handler = {};
const proxy = new Proxy(target, handler);
target.m() // false
proxy.m() // true
Proxy无法代理目标的例子
const _name = new WeakMap();
class Person {
constructor(name) {
_name.set(this, name);
}
get name() {
return _name.get(this);
}
}
const jane = new Person('Jane');
jane.name // 'Jane'
const proxy = new Proxy(jane, {});
proxy.name // undefined
有些原生对象,需要通过this拿到,proxy也无法代理
const target = new Date();
const handler = {};
const proxy = new Proxy(target, handler);
proxy.getDate();
// TypeError: this is not a Date object.
通过this绑定原始对象拿到
const target = new Date('2015-01-01');
const handler = {
get(target, prop) {
if (prop === 'getDate') {
return target.getDate.bind(target);
}
return Reflect.get(target, prop);
}
};
const proxy = new Proxy(target, handler);
proxy.getDate() // 1
实例: Web服务的客户端
Proxy可拦截目标任意属性,适合写Web服务的客户端
const service = createWebService('http://example.com/data');
service.employees().then(json => {
const employees = JSON.parse(json);
// ···
});
这个借口返回各种数据,Proxy可以拦截这个对象的任意属性,不用每种数据写一个方法,写一个Proxy就行了
function createWebService(baseUrl) {
return new Proxy({}, {
get(target, propKey, receiver) {
return () => httpGet(baseUrl+'/' + propKey);
}
});
}