Proxy用于修改某些操作的默认行为,相当于在语言层面做出修改。可以认为是在目标对象之前架设了一层拦截,外界对目标对象的访问,都需要首先经过拦截层
一、示例
我们可以使用Proxy
来拦截一个对象的属性的读写操作:
const realObj = {};
const obj = new Proxy(realObj, {
get(target, key, receiver) {
console.log(`getting ${key}`);
return Reflect.get(target, key, receiver);
},
set(target, key, receiver) {
console.log(`setting ${key}`);
return Reflect.set(target, key, receiver);
}
});
obj.name = 'RuphiLau'; // 输出:setting name
obj.name; // 输出:getting name
二、基本用法
ES6原生提供Proxy构造函数,用来生成Proxy实例,其语法形式为:
let proxy = new Proxy(target, handler)
其中,target是目标对象,而handler则是一个对象,用来定义拦截的行为。要使得拦截起作用,访问的必须是Proxy的实例,而非直接访问目标对象。
Proxy实例是可以作为其他对象的原型对象的,如:
const proxy = new Proxy({}, {
get(target, key) {
return 'Hello';
}
});
const obj = Object.create(proxy);
obj.name; // 'Hello';
一个拦截器对象(handler)中可以包含多个拦截操作,目前可以拦截的操作有:
1)get(target, key, receiver)
拦截属性的读取操作,如:p.foo
、p['foo']
2)set(target, key, value, receiver)
拦截属性的赋值操作,如:p.foo = 123
3)has(target, key)
拦截key in obj
操作,返回布尔值
4)deleteProperty(target, key)
拦截delete obj[key]
操作,返回布尔值
5)ownKeys(target)
拦截Object.getOwnPropertyNames(proxy)
、Object.getOwnPropertySymbols(proxy)
、Object.keys(proxy)
。返回目标对象所有自身的属性的属性名。而在相应操作的时候,则会自动根据枚举性、类型来返回正确的结果
6)getOwnPropertyDescriptor(target)
拦截Object.getOwnPropertyDescriptor(proxy, key)
操作,返回属性的描述对象
7)defineProperty(target, key, desc)
拦截Object.defineProperty(proxy, key, desc)
、Object.defineProperties(proxy, desc)
返回一个布尔值
8)preventExtensions(target)
拦截Object.preventExtensions(proxy)
,返回一个布尔值
9)getPrototypeOf(target)
拦截Object.getPrototypeOf(proxy)
,返回一个对象
10)isExtensible(target)
拦截Object.isExtensible(proxy)
,返回一个布尔值
11)setPrototypeOf(target, proto)
拦截Object.setPrototypeOf(proxy, proto)
,返回一个布尔值
如果拦截对象是函数,那么还有以下的拦截操作:
1)apply(target, ctx, args)
拦截proxy(..args)
、proxy.call(ctx, ...args)
、proxy.apply(ctx, args)
2)construct(target, args)
拦截Proxy实例作为构造函数调用时的操作,如:new proxy(...args)
三、用法详解
1、get()
我们可以借此实现一个生成各种DOM节点的函数dom
:
const dom = new Proxy({}, {
get(target, key, receiver) {
return function(attrs, ...children) {
const el = document.createElement(key);
for (let attr of Object.keys(attrs)) {
el.setAttribute(attr, attrs[attr]);
}
for (let child of children) {
if (typeof child === 'string') {
child = document.createTextNode(child);
}
el.appendChild(child);
}
return el;
}
}
});
const el = dom.div(
{ style: 'width:200px; height:200px; background:#000; color: #FFF;' },
'Hello, wanna go:',
dom.ul(
{},
dom.li({}, 'Zhihu'),
dom.li({}, 'StackOverflow')
)
);
document.body.appendChild(el);
需要注意的是:如果一个属性 不可写,那么该属性就不能被代理:
const target = Object.defineProperties({}, {
foo: {
configurable: false,
writable: false,
value: 123
}
});
const proxy = new Proxy(target, {
get(target, key, receiver) {
return 'abc';
}
})
proxy.foo; // 报错
2、set()
可以拦截属性的赋值操作,可以借此实现很多功能如:数据正确性保证、双向绑定等,还可以实现使_
开头的属性、方法不可访问,如下是一个简单的示例:
function detectPrivate(key, action) {
if (key[0] === '_') {
throw new Error(`Error: Invalid attempt to ${action} private "${key}" property`);
}
}
const target = {
_age: 21,
_name: 'RuphiLau'
}
const proxy = new Proxy(target, {
get(target, key, receiver) {
detectPrivate(key, 'get');
return target[key];
},
set(target, key, value, receiver) {
detectPrivate(key, 'set');
target[key] = value;
return true;
}
});
proxy._age; // Uncaught Error: Error: Invalid attempt to get private "_age" property
注意:如果对象自身的某个属性,不可写也不可配置,那么set
不能改变这个属性的值,只能返回同样的值,否则报错
3、apply()
apply(target, ctx, args)
拦截函数的调用、call和apply操作。其中,ctx表示目标对象的上下文(this),args表示目标对象的参数数组。
const target = function() {
return 'I am the target';
}
const proxy = new Proxy(target, {
apply(target, ctx, args) {
return 'I am the proxy'
}
});
proxy(); // 'I am the proxy'
此外,如果直接调用Reflect.apply
方法,也会被拦截:
Reflect.apply(proxy, null, [1, 2, 3]); // 'I am the proxy'
4、has()
has
方法用来拦截HasProperty操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是in运算符:
const handler = {
has(target, key) {
if (key[0] === '_') {
return false;
}
return key in target;
}
}
const target = {
_age: 21,
name: 'RuphiLau'
};
const proxy = new Proxy(target, handler);
'_age' in proxy; // false
'name' in proxy; // true
注意:
1)如果原对象禁止扩展(Object.preventExtensions(obj)
),这时has拦截会报错:
const obj = { a: 123 }
Object.preventExtensions(obj);
const p = new Proxy(obj, {
has(target, key) {
return false;
}
});
'a' in p; // 报错
2)has
方法拦截的是HasProperty
操作,而非HasOwnProperty
操作,所以has方法不判断一个属性是自身属性,还是继承的属性。
const parent = { b: 456 };
const obj = Object.create(parent);
obj.a = 123;
const p = new Proxy(obj, {
has(target, key) {
return false;
}
});
'a' in p; // false
'b' in p; // false
3)虽然for-in
中也有in
,但是has
对此不生效
const handler = {
has(target, key) {
if (key[0] === '_') {
return false;
}
return key in target;
}
}
const target = {
_age: 21,
name: 'RuphiLau'
};
const proxy = new Proxy(target, handler);
'_age' in proxy; // false
for (let k in proxy) {
console.log(`${k}=${proxy[k]}`);
}
/*
输出:
_age=21
name=RuphiLau
*/
5、construct()
construct方法用于拦截new命令,下面是拦截对象的写法:
const handler = {
construct(target, args, newTarget) {
return new target(...args);
}
}
其中,参数target
表示目标对象,参数args
表示构建函数的参数对象:
const handler = {
construct(target, args, newTarget) {
return {
sum: args.reduce((total, curr) => total + curr)
}
}
};
const Fn = function(){}
const Pfn = new Proxy(Fn, handler);
const p = new Pfn(1, 2, 3, 4);
p.sum; // 10
注意:construct
的返回值必须是一个对象,否则会报错
6、deleteProperty()
deleteProperty()
用于拦截delete
操作,如果方法抛出错误或者返回false,则当前属性就没有办法使用delete
删除:
const handler = {
deleteProperty(target, key) {
return false;
}
}
const obj = { a: 123 };
const p = new Proxy(obj, handler);
delete p.a; // false
p.a; // 123
如果属性的configurable是false,则不能被deleteProperty
方法删除,否则报错
7、defineProperty()
defineProperty
方法拦截了Object.defineProperty()
操作,当返回false
的时候,添加新属性会报错。
注意:如果目标对象不可扩展(extensible),则defineProperty
不能增加目标对象上不存在的属性,否则会报错。此外,如果目标对象的某个属性的writable为false或者configurable为false,则defineProperty
方法不能改变这两个的设置
const obj = {}
const handler = {
defineProperty(target, key, descriptor) {
return false;
}
}
obj.proxy = new Proxy(obj, handler);
Object.defineProperty(obj.proxy, 'name', {
value: 'Ruphi'
}); // 报错
8、getOwnPropertyDescriptor()
拦截Object.getOwnPropertyDescriptor()
,并返回一个属性描述对象或者undefined。
示例:_
开头的表示私有属性,不能获取descriptor
const handler = {
getOwnPropertyDescriptor(target, key) {
if (key[0] === '_') {
return;
}
return Object.getOwnPropertyDescriptor(target, key);
}
}
const obj = { _age: 21, name: 'RuphiLau' }
const proxy = new Proxy(obj, handler);
Object.getOwnPropertyDescriptor(proxy, '_age'); // undefined;
Object.getOwnPropertyDescriptor(proxy, 'name'); // 描述对象
9、getPrototypeOf()
这个方法主要用来拦截获取对象的原型,即会拦截如下的操作:
Object.prototype.__proto__
Object.prototype.isPrototypeOf()
Object.getPrototypeOf()
Reflect.getPrototypeOf()
instanceof
例子如:
const proto = { a: 123 }
const proxy = new Proxy({}, {
getPrototypeOf(target) {
return proto;
}
});
Object.getPrototypeOf(proxy) === proto; // true
proxy.__proto__ === proto; // true
proto.isPrototypeOf(proxy); // true
注意:返回值必须是对象或者null
,否则会报错。如果目标对象不可扩展,则getPrototypeOf
方法必须返回目标对象的原型对象
10、isExtensible()
拦截Object.isExtensible()
操作(返回值只能返回Boolean值,否则会被自动转为Boolean)
使用限制:返回值必须和目标对象的isExtensible
属性保持一致,否则会报错:
const p = new Proxy({}, {
isExtensible(target) {
return false;
}
});
Object.isExtensible(p); // 报错
所以这个方法通常用来输出记录一些信息
11、ownKeys()
拦截对象自身属性的读取操作,即:
Object.getOwnPropertyNames()
Object.getOwnPropertySymbols()
Object.keys()
拦截Object.keys()
:
const target = { a:1, c:3 };
Object.defineProperty(target, 'b', {
enumerable: false,
value: 2
});
const p = new Proxy(target, {
ownKeys(target) {
return ['a', 'b', 'd', Symbol()];
}
});
Object.keys(p); // ['a']
对于ownKeys()
返回数组中的元素,Object.keys()
会过滤以下情况的元素:
- 目标对象中不存在的属性(如
d
) - 属性名为Symbol类型的值
- 不可遍历的属性(enumerable为false)
需要注意的是:ownKeys()
返回的数组,其元素类型只能为string或者symbol类型(因为对象的key只能为这两种类型,如果是其他类型,就会报错)
对于Object.getOwnPropertyNames()
,它会返回ownKeys()
返回值里除了symbol类型外的值:
Object.getOwnPropertyNames(p); // ['a', 'b', 'd']
如果目标对象中有不可配置的属性,那么ownKeys()
中就必须返回,否则会报错:
const target = { a:1, c:3 };
Object.defineProperty(target, 'b', {
configurable: false,
value: 2
});
const p = new Proxy(target, {
ownKeys(target) {
return ['a', 'd'];
}
});
Object.keys(p); // 报错:'ownKeys' on proxy: trap result did not include 'b'
Object.getOwnPropertyNames(p); // 一样报错
此外,如果目标对象是 不可扩展 的,那么ownKeys
方法返回的数组之中,必须 包含原对象的所有属性,且 不能包含多余属性,否则报错
12、preventExtensions()
拦截Object.preventExtensions()
,方法必须返回一个Boolean值,否则会被自动转为Boolean值。
使用注意:只有target对象不可扩展时,才能返回true,否则报错
13、setPrototypeOf()
拦截Object.setPrototypeOf()
方法,返回Boolean值。此外,如果target对象不可扩展,则setPrototypeOf
方法不得改变target对象的原型
四、Proxy.revocable()
该方法返回一个可取消的Proxy实例,其运用场景在于:不允许直接访问目标对象,必须通过代理访问,但是一旦访问结束,就收回代理权:
const target = {};
const handler = {};
const { proxy, revoke } = Proxy.revocable(target, handler);
proxy.foo = 123;
proxy.foo; // 123
revoke();
proxy.foo; // Cannot perform 'get' on a proxy that has been revoked
其中:返回的对象里的proxy
属性表示Proxy实例,而revoke()
方法则用来取消Proxy实例
五、this问题
Proxy所做的代理,不是透明代理。主要原因在于:在Proxy代理的情况下,target对象内部的this会指向Proxy代理,如:
const target = {
foo() {
console.log(this === proxy);
}
};
const handler = {};
const proxy = new Proxy(target, handler);
target.foo(); // false
proxy.foo(); // true
另外就比如此前我们实现_
开头的属性表示私有属性问题:
const person = {
_age: 21,
_name: 'Tom',
desc() {
console.log(`${this._name} is ${this._age}`);
}
}
const limitPrivate = function(key) {
if (key[0] === '_') {
throw new Error(`Invalid access: ${key} is private`);
}
};
const proxy = new Proxy(person, {
get(target, key, reciver) {
limitPrivate(key);
return target[key];
},
set(target, key, value, receiver) {
limitPrivate(key);
target[key] = value;
}
});
proxy._age; // 报错:Invalid access: _age is private
proxy._name; // 报错:Invalid access: _age is private
但是,存在以下的问题:
proxy.desc(); // 报错:Invalid access: _name is private
这是因为,this
此时绑定的是proxy实例,所以在desc()内部,对_age
和_name
的访问相当于proxy._age
和proxy._name
,所以修改如下:
const proxy = new Proxy(person, {
get(target, key, reciver) {
limitPrivate(key);
return typeof target[key] === 'function'
? target[key].bind(target)
: target[key];
},
set(target, key, value, receiver) {
limitPrivate(key);
target[key] = value;
}
});
此时有:
proxy._age; // Invalid access: _age is private
proxy._name; // Invalid access: _name is private
proxy.desc(); // 输出:Tom is 21