概述
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.set(target, key, value, receiver);
}
});
上面的 Proxy
实质上拦截了对象的 get
和 set
方法
obj.count = 1
// setting count!
++obj.count
// getting count!
// setting count!
// 2
ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。
var proxy = new Proxy(target, handler);
Proxy 对象的所有用法,都是上面这种形式,不同的只是 handler
参数的写法。其中,new Proxy()
表示生成一个 Proxy 实例,target
参数表示所要拦截的目标对象,handler
参数也是一个对象,用来定制拦截行为。
var proxy = new Proxy({}, {
get: function(target, property) {
return 35;
}
});
proxy.time // 35
proxy.name // 35
proxy.title // 35
上面代码中,作为构造函数,Proxy
接受两个参数。第一个参数是所要代理的目标对象(上例是一个空对象),即如果没有 Proxy
的介入,操作原来要访问的就是这个对象;第二个参数是一个配置对象,对于每一个被代理的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作。
上面代码中,配置对象有一个 get
方法,用来拦截对目标对象属性的访问请求。get
方法的两个参数分别是目标对象和所要访问的属性。可以看到,由于拦截函数总是返回 35
,所以访问任何属性都得到 35
。
注意,要使得 Proxy
起作用,必须针对 Proxy
实例(上例是 proxy
对象)进行操作,而不是针对目标对象(上例是空对象)进行操作。
一个技巧是将 Proxy 对象,设置到 object.proxy
属性,从而可以在 object
对象上调用。
var object = { proxy: new Proxy(target, handler) };
Proxy 支持的拦截操作:
- get:拦截属性的读取
- set:拦截对象属性的设置
- apply:拦截 Proxy 实例作为函数调用的操作,比如 proxy(...args)
、proxy.call(object, ...args)
、proxy.apply(...)
- 其他参考这里
get
get
方法用于拦截属性的读取参数,接受三个参数,依次为目标对象、属性名和 proxy 实例本身(可选)
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 // 抛出一个错误
get
方法可以继承,可以将 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 createArr(...elements) {
let arr = [...elements];
let handler = {
get: function(target, p, receiver) {
let index = Number(p);
index = index < 0 ? target.length + index : index;
return target[index]
}
};
return new Proxy(arr, handler)
}
let arr = createArr(1, 2, 3, 4);
console.log(arr[-1]); // -3
教程中在 get
方法的结尾多了一个语句,干什么用的不清楚,看完下一章再说
return Reflect.get(target, propKey, receiver);
如果一个属性不可配置(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
// TypeError: Invariant check failed
set()
set()
方法
set
方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选。
假定 Person
对象有一个 age
属性,该属性应该是一个不大于 200
的整数,那么可以使用 Proxy
保证 age
的属性值符合要求。
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 // 报错
同样,如果目标对象自身的某个属性,不可写或不可配置,那么set方法将不起作用。
apply()
方法
apply
方法拦截函数的调用、call
和 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
方法拦截,返回一个字符串。
this
问题
Proxy 代理的情况下,目标对象内部的 this
关键字会指向 Proxy 代理
下面是一个例子,由于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');
jane.name // 'Jane'
const proxy = new Proxy(jane, {});
proxy.name // undefined
上面代码中,目标对象jane的name属性,实际保存在外部WeakMap
上面代码中,目标对象 jane
的 name
属性,实际保存在外部 WeakMap
对象 _name
上面,通过 this
键区分。由于通过 proxy.name
访问时,this
指向 proxy
,导致无法取到值,所以返回 undefined
。
实例:Web 服务的客户端
Proxy 对象可以拦截目标对象的任意属性,这使得它很合适用来写 Web 服务的客户端。
const service = createWebService('http://example.com/data');
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);
}
});
}