飞雪连天射白鹿,笑书神侠倚碧鸳

0%

是时候掌握Proxy了

为其他对象提供一种代理以控制对这个对象的访问

回顾Object.defineProperty

只要准备过面试,相信你一定熟知ES5的Object.defineProperty,遍历属性、get/set劫持监听、双向绑定、重写数组原型支持响应式、balabala
当然Object.defineProperty还有4大属性,configurable、enumerable、writable、value
良好的兼容性

不能一次监听所有属性

1
2
3
4
5
6
7
8
9
10
11
Object.keys(obj).forEach(key => {
Object.defineProperty(obj, key, {
set() {},
get() {}
})
})
// 什么叫简洁啊(战术后仰
obj = new Proxy(obj, {
get() {}
set() {}
})

无法监听新增加的属性

1
2
3
4
// 使用框架语法
Vue.set(obj, "a", "1");
// Proxy直接监听
obj.a = "1";

无法响应数组操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 这也是诟病最多的问题了
const arr = [1, 2, 3];
arr[0] = 10; // 改变已存在的项,都生效
arr[3] = 10; // 改变长度,添加新的项,只有 Proxy 生效
arr.push(10); // push方法,只有 Proxy 生效

// 据说Mobx的默认数组长度是1000,比如下面这样写,离谱的hock,但还是比redux香
[...Array(1000)].forEach((item, index) => {
Object.defineProperty(arr, `${index}`, {
set() {},
get() {}
})
});
arr[30] = 10; // 生效
arr[40] = 10; // 生效

必考题如何重写数组原型用以支持监听

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 在定义变量的时候,判断其是否为数组,如果是数组,那么就修改它的 __proto__,将其指向 subArrProto,从而实现重写原型链
const arrayProto = Array.prototype;
const subArrProto = Object.create(arrayProto);
const methods = ['pop', 'shift', 'unshift', 'sort', 'reverse', 'splice', 'push'];
methods.forEach(method => {
// 重写原型方法
subArrProto[method] = function() {
arrayProto[method].call(this, ...arguments);
};
// 监听新方法
Object.defineProperty(subArrProto, method, {
set() {},
get() {}
})
})

看看Proxy

Proxy 构造函数接收两个参数,第一个参数是需要拦截的目标对象,这个对象只可以是对象、数组或者函数
第二个参数则是一个配置对象,提供了拦截方法,即使这个配置对象为空对象,返回的 Proxy 新实例也不是原来的目标对象(用Reflect获取原目标对象哦)

令人惊叹的13种拦截操作

当然有很多和ts的新语法糖一样令人摸不着头脑能用在何处

get

拦截对目标对象属性的读取 get(目标对象,属性名,Proxy 实例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 设置私有属性,实现禁止访问
const proxy = new Proxy(person, {
get(target, prop) {
if (prop[0] === '_') {
throw new Error(`${prop} is private attribute`);
}
return target[prop]
}
})
proxy._aaa;

// 给对象中未定义的属性设置默认值
const defaults = (obj, initial) => {
return new Proxy(obj, {
get(target, prop) {
if (prop in target) {
return target[prop]
}
return initial
}
})
}
person = defaults(person, 0);
persion.aaa;// 0

set

拦截对属性的赋值操作 set(目标对象,属性名,属性值,Proxy 实例)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 赋值时打印当前状态
set(target, key, value, receiver) {
console.log(`${key} has been set to ${value}`);
Reflect.set(target, key, value);
}
// self属性指向本身
set(target, key, value, receiver) {
if (key === 'self') {
Reflect.set(target, key, receiver);
} else {
Reflect.set(target, key, value);
}
}
// 设置不可写时依旧执行set函数,但不会生效
Object.defineProperty(person, 'name', {
writable: false
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 校验规则扩展
const validators = {
name: {
validate(value) { return value.length > 6; },
message: '用户名长度不能小于6'
},
password: {
validate(value) { return value.length > 10; },
message: '密码长度不能小于10'
},
moblie: {
validate(value) { return /^1(3|5|7|8|9)[0-9]{9}$/.test(value); },
message: '手机号格式错误'
}
}
function validator(obj, validators) {
return new Proxy(obj, {
// 属性拦截
// target => obj
set(target, key, value) {
const validator = validators[key]
if (!validator) {
target[key] = value;
} else if (validator.validate(value)) {
target[key] = value;
} else {
alert(validator.message || "");
}
}
})
}
let form = {};
form = validator(form, validators);
form.name = '666'; // 提示

apply

拦截函数的调用 apply(目标对象,this,参数数组)

1
2
3
4
5
6
7
8
9
10
// 获取函数调用次数,执行耗时
const func = new Proxy(test, {
_count: 0,
// target => test
apply(target, context, args) {
console.log('hello, world');
target.apply(context, args);
}
})
func(); //执行函数也会执行test()

construct

拦截 new 操作符 construct(目标对象,构造函数的参数列表,Proxy 对象)
返回一个对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Person(name, age) {
this.name = name;
this.age = age;
}
const P = new Proxy(Person, {
// target => Person
construct(target, args, newTarget) {
console.log('construct');
return new target(...args);
}
// 或者返回一个新对象
construct(target, args, newTarget) {
return {
name: args[0],
age: args[1]
}
}
})
const p = new P('tom', 21); // 'construct'

构造函数没有返回值或者返回原始类型的值,那么默认返回this, 如果返回引用类型的值,那么最终 new 出来的就是这个值

能做什么

用于代理类

对属性设置 get 拦截,对原型函数设置 apply 拦截

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
say() {
console.log(`my name is ${this.name}, and my age is ${this.age}`)
}
}
const proxyTrack = (targetClass) => {
const prototype = targetClass.prototype;
// 获取原型 prototype 上所有的属性名,forEach 遍历并使用 apply 拦截
Object.getOwnPropertyNames(prototype).forEach((name) => {
targetClass.prototype[name] = new Proxy(prototype[name], {
apply(target, context, args) {
console.time();
target.apply(context, args);
console.timeEnd();
}
})
})
return new Proxy(targetClass, {
// 拦截 construct 方法
construct(target, args) {
const obj = new target(...args);
// 返回一个代理过的对象
return new Proxy(obj, {
get(target, prop) {
console.log(`${target.name}.${prop} is being getting`);
return target[prop]
}
})
}
})
}
const MyClass = proxyTrack(Person); // 先代理
const myClass = new MyClass('tom', 21);// 实例化代理类
myClass.say(); // 查看计算时间
myClass.name;

对象链取值

多层属性可选链 a?.b

普通方式:
`country.province && country.province.city && country.province.city.name;

lodash方式:
_.get(country, 'province.city.name');

可选链:
country?.province?.city?.name

Proxy:
get(country).province.city.name()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const obj={
person:{}
}
let isFirst = true;
function noop() {}
let proxyVoid = get(undefined);
function get (obj) {
if (obj === undefined) {
if (!isFirst) {
return proxyVoid;
}
isFirst = false;
}
return new Proxy(noop, {
// 拦截传入的对象
get(target, prop) {
// return target[prop];
// 不直接返回对象,而是返回代理对象
if (
obj !== undefined &&
obj !== null &&
obj.hasOwnProperty(prop)
) {
return get(obj[prop]);
}
return proxyVoid;
}
// 代理noop,支持返回执行的时候传入的参数
apply(target, context, [arg]) {
return obj === undefined ? arg : obj;
},
})
}
get(obj)() === obj;
get(obj).person(); // {}
get(obj).person.name(); // undefined
get(obj).person.name.xxx.yyy.zzz(); // undefined
听说,打赏我的人最后都找到了真爱
↘ 此处应有打赏 ↙
// 用户脚本