前面的话
Proxy 用于修改某些操作的默认行为,等同于在语言层面上做出修改,所以是一种“元编程”,即对编程语言进行编程。Proxy 可以理解成在目标对象前架设一个“拦截”层,外界对该对象的访问都必须先通过这层拦截,因此提供一种机制可以对外界的访问进行过滤和改写。
创建简单代理
ES6 原生提供Proxy构造函数,使用new Proxy() 生成一个Proxy实例。Proxy构造函数需传递两个参数,一个是target参数(表示所要拦截的目标对象),一个是handler对象,用来定制拦截行为 。 如果handler没有设置任何拦截,那就等同于直接通向原对象。
let target = {};
let proxy = new Proxy(target, {});
proxy.name = 'xiaoqi';
console.log(target.name);// xiaoqi
target.name = 'target';
console.log(target.name);// target
console.log(proxy.name);// target
这个例子,handler没有设置任何拦截,所有操作直接转发到目标对象。将“xiaoqi”赋值给proxy.name属性时会在目标对象上创建name属性,代理只是简单地将操作转发给目标,它不会存储这个属性。由于proxy.name和target.name引用的都是target.name. 因此二者的值相同,从而target.name设置新值后,proxy.name也一同变化
拦截方法
[set()]
set方法用于拦截某个属性的赋值操作。
set()接受4个参数
- target : 代理目标对象
- propKey: 要写入的属性键
- value: 被写入的属性值
- receiver: 操作发生的对象(通常是代理)
假定Person对象有一个age 对象,该属性应该是一个不大于200的整数,那么可以使用Proxy对象保证age的属性值符合要求
let validetor = {
set: function(target, key, value) {
if(key === '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以外的属性,直接保存
target[key] = value;
}
};
let person = new Proxy({},validetor);
person.age = 100;
console.log(person.age);// 100
// person.age = 'young'; // TypeError: The age is not an integer
person.age = 300;// RangeError: The age seems invalid
上面的代码中,由于设置了存值函数set,任何不符合要求的age属性赋值都会抛出一个错误,这是数据验证的一种实现方法。
[get()]
JS有一个时常令人感到困惑的特殊行为,即读取不存在的属性时不会抛出错误,而是用undefined代替被读取的属性值。使用Proxy实例中的get()方法可以在读取属性时检测该属性是否存在。
get()方法用于拦截某个属性懂得读取操作。
get()方法有3个参数:
- target: 被读取属性的源对象(代理目标)
- propKey: 要读取的属性键
- receiver:操作发生的对象
由于get不写入值,所有它复刻了set中的除value外的其它3个参数。
下面是一个拦截读取操作的例子:
var person = {
name: '张三'
};
var proxy = new Proxy(person, {
get: function(target,propKey) {
if(propKey in target) {
return target[propKey];
} else {
throw new ReferenceError("propKey \" "+ propKey + " \"does not exist");
}
}
});
console.log(proxy.name);// 张三
console.log(proxy.age);// propKey " age "does not exist
这个例子中,如果访问目标对象不存在的属性,会抛出一个错误。
get()方法可以继承:
let proxy = new Proxy({}, {
get(target, propKey, receiver) {
console.log('GET ' + propKey);
return target[propKey];
}
});
let obj = Object.create(proxy);
obj.name;// GET name
上面的例子中,obj对象的原型是Proxy实例对象。继承了实例对象的get方法。
使用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');
console.log( arr[-1]);// c
使用get拦截实现一个生成各种DOM节点的通用函数dom:
const dom = new Proxy({},{
get(target, propKey) {
return function(attrs, ...children) {
const el = document.createElement(propKey);
for(let prop of Object.keys(attrs)) {
console.log(prop);
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()方法返回了一个生成DOM的函数。其中执行dom.div()时,代理目标是{},propKey是div,而dom.div()中的参数对应get方法中的参数,第一个参数为attrs,其他的参数为…children。其他的dom. a(),dom.ul(),dom.li()的执行过程与dom.div()一样。
如果一个属性不可配置(configurable)或不可写(writable),则该属性不能被代理,通过Proxy对象访问该属性将会报错。
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;// 报错
[结合使用get和set方法]
有时我们会在对象上设置内部属性,属性名的第一个字符使用下划线开头,表示这些属性不应该被外部使用。
var handler = {
get (target, propKey) {
invariant (propKey, 'get');
return target[propKey];
},
set(target, propKey, value) {
invariant(propKey, 'set');
target[propKey] = value;
return true;
}
};
function invariant (propKey, action) {
if(propKey[0] === '_') {
throw new Error(`Invalid attempt to ${action} private "${propKey}" property`);
}
}
var target = {};
var proxy = new Proxy(target, handler) ;
proxy.prop = 'b';
console.log(proxy.prop); // b
proxy._prop;// Uncaught Error: Invalid attempt to get private "_prop" property
proxy._prop = 'c';// Uncaught Error: Invalid attempt to get private "_prop" property
[apply()]
apply方法拦截函数的调用、call和apply操作.
apply方法可以接受3个参数:
- target:目标对象
- object:目标对象的上下文对象(this)
- args: 目标对象的参数数组。
一个简单的例子:
var target = function () {return 'I am the target'};
var handler = {
apply() {
return 'I am the proxy'
}
}
var p = new Proxy(target, handler);
console.log( p()); // I am the proxy
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);
console.log( proxy(1, 2));// 6
console.log( proxy.call(null, 5, 6));// 22
console.log( proxy.apply(null, [7, 8]));// 30
上面的例子中,proxy是Proxy实例对象,代理的是sum函数,当执行proxy函数(直接调用或apply和call调用),就会被apply方法拦截。
[has()]
has方法用来拦截HasProperty操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是in操作符。
has方法有两个参数:
- target: 目标对象
- propKey:要读取的属性键
下面的例子,使用has方法隐藏某些属性,使其不被in操作符发现:
var handler = {
has(target, propkey) {
if(propkey[0] === '_') {
return false;
}
return propkey in target;
}
}
var target = {_prop: 'foo', prop: 'bar'};
var proxy = new Proxy(target, handler);
console.log( '_prop' in proxy);// false
console.log( 'prop' in proxy);// true;
上面代码中,如果原对象的属性名的第一个字符是下画线。proxy.has就会返回false,从而就不会被in运算符发现。
如果原对象不可配置或禁止扩展,那么此时has拦截会报错:
var obj = {a : 10};
Object.preventExtensions(obj);
var p = new Proxy(obj, {
has(target, prop) {
return false;
}
})
console.log('a' in p) ;// 报错
上面的代码中,obj对象禁止扩展,使用has拦截就会报错。也就是说,如果某个属性不可配置(或者目标对象不可扩展),则has方法就不得 “隐藏”(即返回false)目标对象的该属性。
has拦截对for…in循环不生效:
let stu1 = {name: '张三', score: 59};
let stu2 = {name: '李四', score: 99};
let handler = {
has(target, propkey) {
if(propkey === 'score' && target[propkey] < 60) {
console.log(`${target.name} 不及格`);
return false;
}
return propkey in target;
}
}
let proxy1 = new Proxy(stu1, handler);
let proxy2 = new Proxy(stu2, handler);
console.log( 'score' in proxy1);// 张三不及格 false
console.log('score' in proxy2);// true
for(let a in proxy1) {
console.log(proxy1[a]);
}
// 张三
// 59
for(let a in proxy2) {
console.log(proxy2[a]);
}
// 李四
// 99
由此可以得出has拦截只对in循环有效,对for…in循环不生效。导致不符合要求的属性没有排斥在for…in循环之外
[construct()]
construct方法用于拦截new命令,它接受两个参数:
- target: 目标对象
- args: 构建函数的参数对象
var p = new Proxy(function () {}, {
construct: function(target, args) {
console.log('called: ' + args.join(', ') );
return {value: args[0] * 10}
}
})
console.log( (new p(1, 2, 3)).value);
// called: 1,2,3
// 10
注意:construct方法返回的必须是一个对象,否则会报错。
[deleteProperty()]
deleteProperty方法用于拦截delete操作。如果这个方法抛出错误或者返回false,当前属性就无法被delete命令。
它接受两个参数:
- target: 目标对象
- propKey:要读取的属性键
var handler = {
deleteProperty(target, propKey) {
invariant(propKey, 'delete');
return true;
}
};
function invariant (propKey, action) {
if(propKey[0] === '_') {
throw new Error(` Invalid attempt to ${action} private "${propKey}" property`)
}
}
var target = {_prop: 'foo'};
var proxy = new Proxy(target, handler);
delete proxy._prop;// Uncaught Error: Invalid attempt to delete private "_prop" property
上面的例子中,deleteProperty方法拦截了delete操作符,当删除的属性名第一个字符为“_”时,抛出错误。
[注意]:目标对象自身不可配置(configurable)的属性不能被deleteProperty方法删除,否则会报错。
[defineProperty()]
defineProperty()方法拦截Object.defineProperty操作。
它接受三个参数:
- target: 目标对象
- propKey:要读取的属性键
- propDesc: 属性描述
var handler = {
defineProperty:function(target,key,propDesc) {
console.log(propDesc);
target[key] = propDesc.value;
// return false;
}
};
var target = {};
var proxy = new Proxy(target,handler);
proxy.foo = 'bar'
console.log('proxy添加新的属性:',proxy);
// {value: "bar", writable: true, enumerable: true, configurable: true}
// proxy添加新的属性: Proxy {foo: "bar"}
上面的例子中,可得出Proxy代理内的definneProperty方法return true/false并没有任何意义。
当目标对象添加属性的方法被defineProperty方法拦截,若不添加target[key] = descriptor.value,则不能添加属性。
[getOwnPropertyDescriptor()]
getOwnPropertyDescriptor()方法用来拦截Object.getOwnPropertyDescriptor(),返回一个属性描述对象或者undefined。
Object.getOwnPropertyDescriptor() 方法返回指定对象上一个自有属性对应的属性描述符。(自有属性指的是直接赋予该对象的属性,不需要从原型链上进行查找的属性)
它接受两个参数:
- target: 目标对象
- propKey:要读取的属性键
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);
console.log(Object.getOwnPropertyDescriptor(proxy, 'wat'));// undefined
console.log(Object.getOwnPropertyDescriptor(proxy, '_foo'));// undefined
console.log(Object.getOwnPropertyDescriptor(proxy, 'baz'));// {value: "tar", writable: true, enumerable: true, configurable: true}
[getPrototypeOf()]
getPrototypeOf()方法用来拦截获取对象原型。 它接受一个参数: target 目标对象。具体来说,用于拦截一下操作:
- Object.prototype.proto
- Object.prototype.isPrototypeOf(): 用于测试一个对象是否存在于另一个对象的原型链上
- Object.getPrototypeOf(): 返回指定对象的原型(内部[[Prototype]]属性的值)
- Reflect.getPrototypeOf(): 返回指定对象的原型(内部[[Prototype]]属性的值
- istanceof(): 用于测试构造函数的prototype属性是否出现在对象的原型链中的任何位置
var proto = {};
var p = new Proxy(proto, {
getPrototypeOf(target) {
return target;
}
});
console.log(Object.getPrototypeOf(p) === proto);// true
上面的代码中,getPrototypeOf方法拦截Object.getPrototypeOf(),返回proto对象。
[注意]:getPrototypeOf方法的返回值必须是对象或者null,否则会报错。另外,如果目标对象不可扩展(extensible),getPrototypeOf方法必须返回目标对象的原型对象。
[isExtensible()]
isExtensible方法拦截Object.isExtensible()操作。
Object.isExtensible() 方法判断一个对象是否是可扩展的(是否可以在它上面添加新的属性)。
它接受一个参数:
- target: 目标对象
var p = new Proxy({}, {
isExtensible(target) {
console.log('called');
return true;
}
});
console.log(Object.isExtensible(p));
// called
// true
上面的代码设置了isExtensible方法,在调用Object.isExtensible()时会输出called
[注意]:这个方法只能返回布尔值,否则返回值会被自动转为布尔值
这个方法有一个强限制,它的返回值必须与目标对象的isExtensible属性保持一致,否则就会抛出错误。
Object.isExtensible(proxy) === Object.isExtensible(target);
下面是一个例子:
var p = new Proxy({}, {
isExtensible(target) {
return false
}
});
Object.isExtensible(p);// 报错
[ownKeys()]
ownKeys方法用来拦截对象自身属性的读取操作。具体来说,拦截以下操作:
- Object.getOwnPropertyNames()
- Object.getOwnPropertySymbols()
- Object.keys()
下面是拦截Object.keys()的例子
let target = {
a: 1,
b: 2,
c: 3
};
let handler = {
ownKeys(target) {
return ['a'];
}
};
let proxy = new Proxy(target, handler);
console.log( Object.keys(proxy));
// ['a']
上面的代码拦截了对target对象的Object.keys()操作,只返回a,b,c三个属性中的a属性。
下面是一个拦截第一个字符为下划线的属性名:
let target = {
_bar: 'foo',
_prop: 'bar',
prop: 'baz'
}
let handler = {
ownKeys(target) {
return Reflect.ownKeys(target).filter(function(key){
return key[0] !== '_'
} )
}
}
let proxy = new Proxy(target, handler);
for(let key of Object.keys(proxy)) {
console.log(target[key]);
}
// baz
注意: 使用Object.keys方法时,有三类属性会被ownKeys方法自动过滤,不会返回
- 目标对象上不存在的属性
- 属性名为Symbol值
- 不可遍历的属性
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);
console.log( Object.keys(proxy));// ['a']
上面的代码中,为target对象添加了一个不可遍历的key属性,在handler处理程序中,ownKeys方法返回[‘a’, ‘d’, Symbol.for(‘secret’), ‘key’] 其中只有a不属于上面三个条件之一。其他的结果都过滤掉了。
下面是拦截方法Object.getOwnPropertyNames()的例子:
var p = new Proxy({}, {
ownKeys(target){
return ['a', 'b', 'c'];
}
});
console.log(Object.getOwnPropertyNames(p));
// ['a', 'b', 'c']
ownKeys方法返回的数组成员只能是字符串或Symbol值。如果有其他类型的值,或者返回的根本不是数组,就会报错。
var obj = {}
var p = new Proxy(obj, {
ownKeys(target) {
return [123, true, undefined, null, {}, []];
}
});
Object.getOwnPropertyNames(p); // 报错
如果目标对象自身包含一个配置的属性则该属性必须被ownKeys方法返回,否则会报错:
var obj = {};
Object.defineProperty(obj, 'a', {
configurable: false,
enumerable: true,
value: 10
});
var p = new Proxy(obj, {
ownKeys(target) {
return ['b']
}
})
Object.getOwnPropertyNames(p); // Uncaught TypeError: 'ownKeys' on proxy: trap result did not include 'a'
上面的代码中,obj对象的a属性时不可配置,这时ownKeys方法返回的数组之中必须包含原对象所有属性,且不能包含多余的属性。
[preventExtensions()]
Object.preventExtensions()方法让一个对象变的不可扩展,也就是永远不能再添加新的属性。
preventExtensions方法拦截Object.preventExtensions()。该方法必须返回一个布尔值,否则会被自动转为布尔值。
它接受一个参数:
- target: 目标对象
这个方法有一个限制,只有目标对象不可扩展时,即Object.isExtensible(proxy)为false,proxy.preventExtensions()才能返回true,否则会报错。
var p = new Proxy({}, {
preventExtensions(target) {
return true;
}
});
Object.preventExtensions(p); // 报错
上面的代码中,proxy.preventExtensions方法返回true,但此时Object.isExtensible(proxy)会返回true,因此报错。
为防止出现这个问题,通常要在proxy.preventExtensions方法中调用一次Object.preventExtensions,代码如下:
var p = new Proxy({}, {
preventExtensions(target) {
console.log('called');
Object.preventExtensions(target);
return true;
}
})
Object.preventExtensions(p);
// called
// true
[setPrototypeOf()]
setPrototypeOf方法主要用于拦截Object.setPrototypeOf()方法.
它接受两个参数:
- target: 目标对象
- proto: 指定的对象原型
Object.setPrototypeOf() 方法设置一个指定的对象的原型 ( 即, 内部[[Prototype]]属性)到另一个对象或null。
var handler = {
setPrototypeOf(target, proto) {
throw new Error('Changing the prototype is forbidden');
}
}
var proto = {};
var target = function () {};
var proxy = new Proxy(target, handler);
Object.setPrototypeOf(proxy, proto);// Uncaught Error: Changing the prototype is forbidden
注意:该方法只能返回布尔值,否则会被自动转为布尔值。另外,如果目标对象不可扩展,setPrototypeOf方法不得改变目标对象的
原型。
[proxy.revocable()]
proxy.revocable()返回一个可取消的Prox实例。
let target = {};
let handler = {};
let {proxy, revoke} = Proxy.revocable(target, handler);
proxy.foo = 123;
console.log(proxy.foo);// 123
revoke();
console.log(proxy.foo);// 报错
Proxy.revocable()方法返回一个对象,其中proxy是Proxy实例,revoke属性是一个函数,可以取消Proxy实例。 当执行revoke函数后再访问Proxy实例,就会报错。
this问题
虽然Proxy可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下也无法保证与目标对象的行为一致。主要原因就是在Proxy代理的情况下,目标对象内部this关键字会指向Proxy代理
const target = {
m(){
console.log(this === proxy);
}
}
const handler = {};
const proxy = new Proxy(target, handler);
target.m()// false
proxy.m()// true
上面的代码中,一旦proxy代理target.m,后者内部的this就指向proxy,而不是target。
下面一个例子,由于this指向的变化导致Proxy无法代理目标对象:
const _name = new WeakMap();
class Person {
constructor(name) {
_name.set(this, name);
}
get name() {
return _name.get(this);
}
}
const jane = new Person('Jane');
console.log( jane.name);// Jane
const proxy = new Proxy(jane, {});
console.log( proxy.name);// undefined
上面的代码中,目标对象jane的name属性实例保存在外部对象_names上面,给_name对象添加this键,保存name属性值。由于proxy.name访问时,this指向proxy,导致无法取到值。
此外,有些原生对象的内部属性只有通过正确的this才能获取,所以Proxy也无法代理这些原生对象的属性:
const target = new Date();
const handler = {};
const proxy = new Proxy(target, handler);
proxy.getDate();// Uncaught TypeError: this is not a Date object
上面的代码中,getDate方法只能在Date对象实例上面获取,如果this不是Date对象实例就会报错。这时,this绑定原始对象就可以解决这个问题。
const target = new Date('2019-08-20');
const handler = {
get(target, propKey) {
if(propKey === 'getDate') {
return target.getDate.bind(target);
}
return Reflect.get(target, propKey);
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.getDate());// 20
实例:web服务的客户端
Proxy对象可以拦截目标对象的任意属性,这使得它很合适用来编写Web 服务的客户端。
const service = createWebService('http://example.com/date');
service.employees().then(json => {
const employees = JSON.parse(json);
// ...
})
上面的代码新建了一个web服务的接口,这个接口返回各种数据。Proxy可以拦截这个对象的任意属性,所以不用为每个数据写一个适配方法,只要写一个Proxy拦截即可。
function createWebService(baseUrl) {
return new Proxy({}, {
get(target, propkey, receiver) {
return ()=> httpGet(baseUrl + '/' + propKey);
}
})
}
同理,Proxy也可以用来实现数据库的ORM层。