JS 中实现代理的语法:
let target = {};
let proxy = new Proxy(target, {});
proxy.a = 1;
console.log(proxy.a);
console.log(target.a);
for(let i in proxy){
console.log(i);
}
// 1
// 1
// a
new Proxy接收两个参数,要代理的目标对象以及代理行为,这里代理行为使用的是空对象,因此所有对代理对象属性的读写都是由被代理的原始对象承担的。
get
可以设置代理行为的 get 方法以改变读取代理对象属性时的行为,比如:
let numbers = [1, 2, 3, 4, 5];
numbers = new Proxy(numbers, {
get(target, index) {
if(index in target){
return target[index];
}
return 0;
}
});
console.log(numbers[1]);
console.log(numbers[100]);
// 2
// 0
在正常情况下,JS 中读取一个数组的不存在的下标,会返回undefined,这里使用代理改变了获取数组元素的行为,如果下标不存在,返回的将是 0。
再看一个例子:
let dictionary = {
'hello': '你好',
'world': '世界'
}
dictionary = new Proxy(dictionary, {
get(target, phrase) {
if (phrase in target) {
return target[phrase];
}
return phrase;
}
});
console.log(dictionary['hello']);
console.log(dictionary['world']);
console.log(dictionary['universe']);
// 你好
// 世界
// universe
读取字典中不存在的索引,同样会返回undefined,使用代理改变字典对象的行为,如果查询不存在的索引,返回的是索引本身。
set
使用 set 可以改变代理对象写入属性的行为:
let numbers2 = [];
numbers2 = new Proxy(numbers2, {
set(target, index, value) {
if (typeof value === 'number') {
target[index] = value;
return true;
}
return false;
}
});
numbers2.push(1);
numbers2.push(2);
console.log(numbers2);
// [ 1, 2 ]
numbers2.push('a');
// TypeError: 'set' on proxy: trap returned falsish for property '2'
这里代理的目标对象是一个数组,并在代理对象中使用set方法检查添加属性值是否是数字类型,如果是就添加,不是就报错(set方法返回false)。
代理的set方法中只需要直接给原始对象的属性赋值即可,JS 预定义的push等方法都可以和set方法配合的很好。
ownKeys
前面说过,JS 默认约定以下划线开始的属性是私有属性,不应该被对象外部直接访问,但 JS 语言本身并不提供语法层面的保护,外部代码依然可以访问和遍历。
我们可以通过代理实现保护,不让外部代码遍历到这些属性:
let user = {
name: '张三',
age: 20,
_password: '123'
};
user = new Proxy(user, {
ownKeys(target) {
return Object.keys(target).filter(key => !key.startsWith('_'));
}
});
for (let key in user) {
console.log(key);
}
// name
// age
如果仅用于隐藏某些属性,这样做已经足够,但如果要给代理对象添加某些原始对象不存在的属性:
let user2 = {
name: '张三',
age: 20,
_password: '123'
};
user2 = new Proxy(user2, {
ownKeys(target) {
let keys = Object.keys(target);
return [keys, 'a', 'b', 'c'];
}
});
for (let key in user2) {
console.log(key);
}
// name
// age
// _password
可以看到,遍历时并没有通过ownKeys添加的新属性(a,b,c)。这是因为 JS 在获取可遍历的属性时(Object.keys(...)),会通过内部方法[[GetOwnProperty]]获取属性的描述符,查看其是否是可遍历的属性,上面的示例中通过ownKeys方法添加的新属性没有属性描述符,因此这里遍历不到。
可以通过getOwnPropertyDescriptor方法改变内部方法[[GetOwnProperty]]的行为:
// ...
user2 = new Proxy(user2, {
ownKeys(target) {
let keys = Object.keys(target);
return [keys, 'a', 'b', 'c'];
},
getOwnPropertyDescriptor(target, key) {
if (key in target) {
return Object.getOwnPropertyDescriptor(target, key);
}
if (['a','b','c'].includes(key)) {
return {
value: 0,
writable: true,
enumerable: true,
configurable: true
};
}
}
});
for (let key in user2) {
console.log(key);
}
// name
// age
// _password
// a
// b
// c
deleteProperty
可以使用上述方法结合deleteProperty方法实现一个对私有属性进行保护的代理:
function propertyProtectProxy(obj){
return new Proxy(obj, {
get(target, key) {
if (key.startsWith('_')) {
throw new Error(`属性 ${key} 是受保护的`);
}
let value = target[key];
return typeof value === 'function' ? value.bind(target) : value;
},
set(target, key, value) {
if (key.startsWith('_')) {
throw new Error(`属性 ${key} 是受保护的`);
}
target[key] = value;
return true;
},
deleteProperty(target, key) {
if (key.startsWith('_')) {
throw new Error(`属性 ${key} 是受保护的`);
}
delete target[key];
return true;
},
ownKeys(target) {
return Object.keys(target).filter(key => !key.startsWith('_'));
}
});
}
let user3 = {
name: '张三',
age: 20,
_password: '123'
};
user3 = propertyProtectProxy(user3);
console.log(user3.name);
console.log(user3.age);
console.log(user3._password);
// 张三
// 20
// Error: 属性 _password 是受保护的
需要注意的是,get方法需要考虑获取的属性是不是函数,如果是函数,需要使用func.bind()将函数对象绑定到原始对象(target),这是因为这里获取到的函数对象的上下文中this对应的是代理对象,如果不将其重新绑定到原始对象,该方法中就无法正常使用私有属性(_开头的属性):
let user4 = {
name: '张三',
age: 20,
_password: '123',
checkPassword(password) {
return this._password === password;
}
};
user4 = propertyProtectProxy(user4);
console.log(user4.checkPassword('123'));
// true
has
in运算符的行为可以通过代理行为的has方法改变:
let range = {
from: 1,
to: 5
}
range = new Proxy(range, {
has(target, key) {
return key >= target.from && key <= target.to;
}
});
for (let i = 0; i <= 10; i++) {
if (i in range) {
console.log(i);
}
}
// 1
// 2
// 3
// 4
// 5
包装函数
之前展示过如何包装函数:
function displayPerson(name, age){
console.log(`Name is ${name} and age is ${age}`);
}
function delayFunc(func, wait){
return function(args){
setTimeout(() => {
func.apply(this, args);
}, wait);
}
}
displayPerson = delayFunc(displayPerson, 1000);
displayPerson('Tom', 20);
// Name is Tom and age is 20
这样做有一个缺陷,包装后的函数会丢失一些原始函数的信息,比如:
console.log(displayPerson.name);
console.log(displayPerson.length);
// displayPerson
// 2
displayPerson = delayFunc(displayPerson, 1000);
console.log(displayPerson.name);
console.log(displayPerson.length);
//
// 0
可以使用代理对象包装函数:
function displayPerson(name, age){
console.log(`Name is ${name} and age is ${age}`);
}
function delayFunc(func, wait){
return new Proxy(func, {
apply(target, thisArg, args){
setTimeout(() => {
target.apply(thisArg, args);
}, wait);
}
});
}
console.log(displayPerson.name);
console.log(displayPerson.length);
// displayPerson
// 2
displayPerson = delayFunc(displayPerson, 1000);
console.log(displayPerson.name);
console.log(displayPerson.length);
// displayPerson
// 2
displayPerson('Tom', 20);
// Name is Tom and age is 20
Reflect
Reflect是一个内置对象,利用它可以简化代理对象的创建过程:
let user = {
name: '张三',
age: 20,
}
user = new Proxy(user, {
get(target, key) {
return Reflect.get(target, key);
},
set(target, key, value) {
return Reflect.set(target, key, value);
}
})
console.log(user.name);
console.log(user.age);
这和使用索引(target[key])效果是相同的,但有额外的好处,比如如果代理对象存在原型继承:
let user = {
_name: '张三',
get name(){
return this._name;
}
}
user = new Proxy(user, {
get(target, key) {
return target[key];
},
})
let admin = {
_name: '李四',
__proto__: user
}
console.log(admin.name);
// 张三
这个例子中存在原型继承,admin 对象通过原型继承代理对象,访问代理对象属性(admin.name)时,该属性不存在,所以通过原型链上的代理对象检索属性,而代理对像通过target[key]返回原始对象的name属性,该属性是一个访问器属性,返回的是this._name,所以最后获取到的是张三,而不是期望的李四。
如果使用Reflect,结果就会不同:
let user = {
_name: '张三',
get name(){
return this._name;
}
}
user = new Proxy(user, {
get(target, key, receiver) {
return Reflect.get(target, key, receiver);
},
})
let admin = {
_name: '李四',
__proto__: user
}
console.log(admin.name);
// 李四
这个例子中get多接收了一个receiver参数,该参数保存了正确的this,因此最后获取到了期望的结果。
局限性
内部槽
一些预定义类型数据存储在只能内部访问的特殊属性中,比如Map访问的是内部槽[[MapData]]中的数据:
let map = new Map();
let proxy = new Proxy(map, {});
proxy.set('name', '张三');
// TypeError: Method Map.prototype.set called on incompatible receiver #<Map>
这里报错是因为代理对象并没有[[MapData]],因此报错。
这个问题可以通过修改代理行为,获取代理对象方法时重新绑定this到原始对象:
let map = new Map();
let proxy = new Proxy(map, {
get(target, key, receiver) {
let value = Reflect.get(arguments);
return typeof value === 'function' ? value.bind(target) : value;
},
});
proxy.set('name', '张三');
console.log(proxy.get('name'));
私有字段
代理对象不能访问原始对象的私有属性:
class Person {
#name = '张三';
getName() {
return this.#name;
}
}
let person = new Person();
person = new Proxy(person, {});
console.log(person.getName());
// TypeError: Cannot read private member #name from an object whose class did not declare it
同样可以通过绑定this的方式解决:
class Person {
#name = '张三';
getName() {
return this.#name;
}
}
let person = new Person();
person = new Proxy(person, {
get(target, key, receiver) {
let value = Reflect.get(arguments);
return typeof value === 'function' ? value.bind(target) : value;
}
});
console.log(person.getName());
可撤销代理
可以创建一个随时可以撤销的代理:
let obj = {
data: 'hello'
}
let { proxy, revoke } = Proxy.revocable(obj, {});
console.log(proxy.data);
revoke();
console.log(proxy.data);
// TypeError: Cannot perform 'get' on a proxy that has been revoked
在调用revoke方法后,代理对象中对原始对象的引用会被删除,因此所有的相关访问都会失败。
The End.
本文的完整示例可以从获取。
参考资料

文章评论