[TOC]
用Proxy进行预处理
什么是钩子函数?当我们在操作一个对象或者方法时会有几种动作,比如:在运行函数前初始化一些数据,在改变对象值后做一些善后处理。这些都算钩子函数,Proxy的存在就可以让我们给函数加上这样的钩子函数,你也可以理解为在执行方法前预处理一些代码。你可以简单的理解为他是函数或者对象的生命周期。
概述
Proxy用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种元编程
(meta programming),即对编程语言进行编程。
Proxy可以理解成,在目标对象之前架设一层拦截,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为代理器
。
ES6原生提供Proxy构造函数,用来生成Proxy实例。
const proxy = new Proxy(target, handler);
Proxy对象的所有用法,都是上面这种形式,不同的只是handler
参数的写法。其中,new Proxy()表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。
Proxy拦截操作(共有13种)
1. get(target, propKey, receiver(可选参数))
get方法用于:拦截对象某个属性的读取操作,可以接受三个参数,依次为目标对象、属性名和proxy实例本身(严格地说,是操作行为所针对的对象),其中最后一个参数可选。
const obj = {
name: 'lisi'
}
const p = new Proxy(obj, {
get: function(target, key, receiver) {
if (key in target) {
return target[key];
} else {
throw new ReferenceError("Property \"" + key + "\" does not exist.");
}
}
});
console.log(p.name); // lisi
console.log(p.age); // ReferenceError: Property "age" does not exist.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2. set(target, propKey, value, receiver)
拦截对象属性的设置,最后返回一个布尔值。
const handler = {
// 对设置属性进行拦截
set: function(target, key, value, receiver) {
if (key === 'name') {
target[key] = receiver; // 将name属性值设置为当前操作行为所针对的对象
} else {
target[key] = value;
}
}
}
const proxy = new Proxy({}, handler);
const obj = {};
// 设置obj的原型是proxy
Object.setPrototypeOf(obj, proxy);
obj.name = 'lisi';
obj.age = 12;
console.log(obj.name === obj); // true
console.log(obj.age); // 12
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
3. apply(target, object, args)
apply方法拦截函数的调用、call和apply操作。
apply方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组。
const handler = {
apply(target, ctx, args) {
console.log(target);
console.log(ctx);
console.log(args); // [1, 2]
return Reflect.apply(...arguments) * 3;
}
};
function sum(left, right) {
return left + right;
}
const proxy = new Proxy(sum, handler);
// 执行proxy函数(直接调用或call和apply调用),就会被apply方法拦截
console.log(proxy(1, 2)); // 9
console.log(proxy.call(null, 2, 3)); // 15
console.log(proxy.apply(null, [3, 4])); // 21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
4. has(target, prop)
拦截例如prop in proxy
的操作,返回一个布尔值。
has方法用来拦截HasProperty操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是in运算符。
has方法可以接受两个参数,分别是目标对象、需查询的属性名。
const handler = {
has(target, key) {
// 隐藏私有属性
if (key.startsWith('_')) {
return false;
}
return key in target;
}
};
const proxy = new Proxy({name: 'lisi', _age: 18}, handler);
console.log('_age' in proxy); // false
console.log('name' in proxy); // true
2
3
4
5
6
7
8
9
10
11
12
5. construct(target, args)
construct方法用于拦截new命令,下面是拦截对象的写法。
6. isExtensible(target)
拦截Object.isExtensible(proxy)
,返回一个布尔值
Object.isExtensible()方法判断一个对象是否是可扩展的(是否可以在它上面添加新的属性)。
const obj = {name: 'lisi'};
console.log(Object.isExtensible(obj)); // true
// 设置obj不可扩展
Object.preventExtensions(obj);
console.log(Object.isExtensible(obj)); // false
2
3
4
5
6
7. deleteProperty(target, prop)
拦截例如delete proxy[prop]
的操作,返回一个布尔值。
8. ownKeys(target)
拦截Object.getOwnPropertyNames(proxy)、Object.keys(proxy)、for in循环等操作,最终会返回一个数组。
9. getOwnPropertyDescriptor(target, prop)
拦截 Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
10. defineProperty(target, propKey, propDesc)
拦截 Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
11. preventExtensions(target)
拦截 Object.preventExtensions(proxy),返回一个布尔值。
12 .getPrototypeOf(target)
拦截 Object.getPrototypeOf(proxy),返回一个对象。
13. setPrototypeOf(target, proto)
拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
Proxy vs Object.defineProperty
在Proxy出现之前,js通过Object.defineProperty,允许对对象的getter/setter进行拦截,那么两者的区别在哪里呢? 区别如下:
- Object.defineProperty不能一次性监听所有属性
- Object.defineProperty无法监听新增加的属性
- Object.defineProperty无法响应数组操作
- Proxy拦截方式更多
- Object.defineProperty兼容性更好
Object.defineProperty不能监听所有属性
Object.defineProperty 无法一次性监听对象所有属性,必须遍历或者递归来实现。
let girl = {
name: "marry",
age: 22
}
// Proxy监听整个对象
girl = new Proxy(girl, {
get() {}
set() {}
})
// Object.defineProperty
Object.keys(girl).forEach(key => {
Object.defineProperty(girl, key, {
set() {},
get() {}
})
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Object.defineProperty无法监听新增加的属性
Proxy 可以监听到新增加的属性,而 Object.defineProperty 不可以,需要你手动再去做一次监听。因此,在 Vue 中想动态监听属性,一般用 Vue.set(girl, "hobby", "game") 这种形式来添加。
let girl = {
name: "marry",
age: 22
}
// Proxy监听整个对象
girl = new Proxy(girl, {
get() {}
set() {}
})
// Object.defineProperty
Object.keys(girl).forEach(key => {
Object.defineProperty(girl, key, {
set() {},
get() {}
})
});
// Proxy生效,Object.defineProperty不生效
girl.hobby = "game";
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Proxy拦截方式更多
Proxy 提供了13种拦截方法,包括拦截 constructor、apply、deleteProperty 等等,而 Object.defineProperty 只有 get 和 set。
Object.defineProperty兼容性更好
Proxy 是新出的 API,兼容性还不够好,不支持 IE 全系列。
Proxy应用
demo1
let obj = {
a: 1,
b: 2
};
const p = new Proxy(obj, {
// target即obj
get(target, key, value) {
if(key in target) {
return target[key]
}
else {
return '自定义结果'
}
}
});
console.log(obj.a); // 1
console.log(obj.c); // undefined
console.log(p.a); // 1
console.log(p.c); // 自定义结果
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const person = {
name: 'lisi',
age: 12
}
const personProxy = new Proxy(person, {
// get接收两个参数
get(target, key) {
return target[key].toUpperCase();
},
// set接收三个参数
set(target, key, value) {
if (typeof value === 'string') {
target[key] = value.trim();
}
}
});
console.log(personProxy.name); // LISI
personProxy.hobbies = ' 🏀 ';
console.log(personProxy.hobbies);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
demo2
const phoneNumber = new Proxy({}, {
set(target, key, value) {
target[key] = value.match(/[0-9]/g).join('');
},
get(target, key) {
return target[key].replace(/(\d{3})(\d{4})(\d{4})/, '$1-$2-$3');
}
});
phoneNumber.home = '131 1122 9876'; // 3 4 4形式
phoneNumber.company = '134 342 96876'; // 3 3 5形式
// { home: '13111229876', company: '13434296876' }
// console.log(phoneNumber);
console.log(phoneNumber.home);
console.log(phoneNumber.company);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
demo3
const safetyProxy = new Proxy({id: 2}, {
set(target, key, value) {
const likeKey = Object.keys(target).find(k => k.toLowerCase() === key.toLowerCase());
// 如果target中不存在key和与key相似的key
if (!(key in target) && likeKey) {
throw new Error('已经存在相似的key');
}
target[key] = value;
}
});
// safetyProxy.Id = 3;
safetyProxy.name = 'lisi';
2
3
4
5
6
7
8
9
10
11
12
13