官网解释:
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。
为了方便大家好理解,这里先抓几个关键词出来:
- 对象
- 创建对象代理
- 拦截
- 自定义
从上面的关键词大家应该能够揣摩个一二了,首先 Proxy 是一个对象,它可以给另外一个对象创建一个代理,代理可以简单理解为代理某一个对象,然后通过这个代理对象,可以针对于该对象提前做一些操作,比如拦截等。
通俗的解释:
假如我们有一个对象 obj,使用 Proxy 对象可以给我们创建一个代理,就好比我们打官司之前,可以先去律师事务所找一个律师,律师全权代理我们。有了这个代理之后,就可以对这个 obj 做一些拦截或自定义,比如对方想要直接找我们谈话时,我们的律师可以先进行拦截,他来判断是否允许和我谈话,然后再做决定。这个律师就是我们对象的代理,有人想要修改 obj 对象,必须先经过律师那一关。
基本概念其实不复杂,有些小伙伴不太理解的原因大多是平时写代码的时候,以为对象就是一个独立的变量,比如声明了一个对象 obj={name:"小猪课堂"},我们通常也不会去做什么拦截,想改就改。
这就是一个惯性思维!
2.如何使用
既然我们知道了 Proxy 的作用,那么我们如何使用它呢?或者说如何给一个对象创建代理。
Proxy 的使用非常简单,我们可以使用 new 关键字实例化它。
代码如下:
const p = new Proxy(target, handler)
代码非常的简单,重点是我们需要掌握 Proxy 接收的参数。
参数说明
target:
需要被代理的对象,它可以是任何类型的对象,比如数组、函数等等,注意不能是基础数据类型。
示例代码:
<script>
let obj = {
name: '小猪课堂',
age: 23
}
let p = new Proxy(obj, handler);
</script>
handler:
它是一个对象,该对象的属性通常都是一些函数,handler 对象中的这些函数也就是我们的处理器函数,主要定义我们在代理对象后的拦截或者自定义的行为。handler 对象的的属性大概有下面这些,具体使用方法我们在后面章节详解:
- handler.apply()
- handler.construct()
- handler.defineProperty()
- handler.deleteProperty()
- handler.get()
- handler.getOwnPropertyDescriptor()
- handler.getPrototypeOf()
- handler.has()
- handler.isExtensible()
- handler.ownKeys()
- handler.preventExtensions()
- handler.set()
- handler.setPrototypeOf()
我们使用 new 关键词后生成了一个代理对象 p,它就和我们原来的对象 obj 一样,只不过它是一个 Proxy 对象,我们打印出来看看就能更好理解了。
示例代码:
<script>
let obj = {
name: '小猪课堂',
age: 23
}
let p = new Proxy(obj, {});
console.log(obj);
console.log(p);
</script>
输出结果:
3.Handler 对象详解
上一节的使用只是简单的初始化了一个代理对象,而我们需要重点掌握的是 Proxy 对象中的 handler 参数。因为我们所有的拦截操作都是通过这个对象里面的函数而完成的。
就好比律师全权代理了我们,那他拦截之后能做什么呢?或者说律师拦截之后他有哪些能力呢?这就是我们 handler 参数对象的作用了,接下来我们就一一来讲解下。
3.1 handler.apply
该方法主要用于函数调用的拦截,比如我们代理的对象是一个函数,那么我们代理这个函数之后,可以在它调用之前做一些我们想做的事。
语法:
// 函数拦截
let p1 = new Proxy(target, {
apply: function (target, thisArg, argumentsList) {
}
});
参数解释:
- target:被代理对象,也就是目标函数
- thisArg:调用时的上下文对象,也就是 this 指向,它绑定在 handler 对象上面
- argumentsList:函数调用的参数数组
使用案例:
function sum(a, b) {
return a + b;
}
let p1 = new Proxy(sum, {
apply: function (target, thisArg, argumentsList) {
return argumentsList[0] + argumentsList[1] * 100;
}
});
// 正常调用
console.log(sum(1, 2)); // 3
// 代理之后调用
console.log(p1(1, 2)); // 201
上段代码中我们代理了 sum 函数对象,并产生了新的 p1 代理对象,在 p1 代理对象里面,我们对函数的调用做了拦截,让它返回了新的值。
注意:
我们这里代理的函数对象必须是可调用的,也就是 target 可调用,否则会报错。
3.2 handler.construct
该方法主要是用于拦截 new 操作符的,我们通常使用 new 操作符都是在函数的情况下,但是我们不能说 new 操作符只能作用与函数,确切的说 new 操作符必须作用于自身带有[[Construct]]内部方法的对象上,而这种对象通常就是函数,总之一句话,使用 new targe 是必须有效的。
语法:
// 构造函数拦截
let p2 = new Proxy(target, {
construct: function (target, argumentsList, newTarget) {
}
});
参数解释:
- target:被代理对象,需要能够使用 new 操作符初始化它的实例,通常就是一个函数
- argumentsList:使用 new 操作符是传入的参数列表
- newTarget:被调用的构造函数,也就是 p2
使用案例:
let p2 = new Proxy(function () { }, {
construct: function (target, argumentsList, newTarget) {
return { value: '我是' + argumentsList[0] };
}
});
console.log(new p2("小猪课堂")); // {value: '我是小猪课堂'}
上段代码中 p2 就是一个构造函数,只不过是代理之后的新函数,我们使用 new 操作符实例化它的,首先就会去执行 handler 里面的 construct 方法。
注意:
这里有两个点需要大家注意
- target 必须能够使用 new 操作符初始化
- construct 必须返回一个对象
3.3 handler.defineProperty
这个方法其实比较有意思,Object.defineProperty 方法本身就有拦截对象的意思在里面,但是我们的 Proxy 对象可以正针对 Object.defineProperty 操作进行拦截,对于 Object.defineProperty 方法不熟悉的同学可以先去学学。
语法:
// 拦截 Object.defineProperty
let p3 = new Proxy(target, {
defineProperty: function (target, property, descriptor) {
}
});
参数解释:
- target:被代理对象
- property:属性名,也就是当我们使用 Object.defineProperty 操作的对象的某个属性
- descriptor:待定义或修改的属性的描述符
使用案例:
let p3 = new Proxy({}, {
defineProperty: function (target, property, descriptor) {
descriptor.enumerable = false; // 修改属性描述符
console.log(property, descriptor);
return true;
}
});
let desc = { configurable: true, enumerable: true, value: 10 };
Object.defineProperty(p3, 'a', desc); // a {value: 10, enumerable: false, configurable: true}
上段代码中我们使用 Proxy 代理了一个空对象,并产生了新的代理对象 p3,当使用 Object.defineProperty 操作 p3 对象时,就会触发 handler 中的 defineProperty 方法。
注意:
- 被代理的对象必须要能被扩展
- hanlder 中的 defineProperty 方法必须返回一个 Boolean 值
- 不能添加或者修改一个属性为不可配置的,如果它不作为一个目标对象的不可配置的属性存在的话
3.4 handler.deleteProperty
该方法用于拦截对对象属性的 delete 操作,我们经常使用 delete 删除对象中的某个属性,我们可以使用 deleteProperty 方法对该做进行拦截。
语法:
let p4 = new Proxy(target, {
deleteProperty: function (target, property) {
}
});
参数解释:
- target:被代理的目标对象
- property:将要被删除的属性
使用案例:
let p4 = new Proxy({}, {
deleteProperty: function (target, property) {
console.log("将要删除属性:", property)
}
});
delete p4.a; // 将要删除属性:a
当我们删除 p4 对象的属性时,便会执行 handler 中的 deleteProperty 方法。
注意:
代理的目标对象的属性必须是可配置的,即可以删除,否则会报错。
3.5 handler.get
该方法用于拦截对象的读取属性操作,比如我们要读取某个对象的属性,就可以使用该方法进行拦截。
语法:
// 拦截读取属性操作
let p5 = new Proxy(target, {
get: function (target, property, receiver) {
}
});
参数解释:
- target:被代理的目标对象
- property:想要获取的属性名
- receiver:Proxy 或者继承 Proxy 的对象
使用案例:
// 拦截读取属性操作
let p5 = new Proxy({}, {
get: function (target, property, receiver) {
console.log("属性名:", property); // 属性名:name
console.log(receiver); // Proxy {}
return '小猪课堂'
}
});
console.log(p5.name); // 小猪课堂
可以看到我们代理的对象其实是一个空对象,但是我们获取 name 属性是是返回了值的,其实是在 handler 对象中的 get 函数返回的。
注意:
代理的对象属性必须是可配置的,get 函数可以返回任意值。
3.6 handler.getOwnPropertyDescriptor
该方法用于拦截 Object.getOwnPropertyDescriptor 操作,也可以说它是该方法的钩子,如果对 getOwnPropertyDescriptor 还不熟悉的小伙伴可以先去了解一下。
语法:
let p6 = new Proxy(target, {
getOwnPropertyDescriptor: function (target, prop) {
}
});
参数解释:
- target:被代理的目标对象
- prop:返回属性名称的描述
使用案例:
let p6 = new Proxy({ name: '小猪课堂' }, {
getOwnPropertyDescriptor: function (target, prop) {
console.log('属性名称:' + prop); // 属性名称:name
return { configurable: true, enumerable: true, value: '张三' };
}
});
console.log(Object.getOwnPropertyDescriptor(p6, 'name').value); // 张三
上段代码中我们在拦截其中重新设置了属性描述,所以最后打印的 value 是”张三“。
注意:
- getOwnPropertyDescriptor 必须返回一个 object 或 undefined。
- 使用 getOwnPropertyDescriptor 时,目标对象的该属性必须存在
3.7 handler.getPrototypeOf
当我们读取代理对象的原型时,会触发 handler 中的 etPrototypeOf 方法。
语法:
let p7 = new Proxy(obj, {
getPrototypeOf(target) {
}
});
参数解释:
- target:被代理的目标对象
使用案例:
let p7 = new Proxy({}, {
getPrototypeOf(target) {
return { msg: "拦截获取对象原型操作" }
}
});
console.log(p7.__proto__); // {msg: '拦截获取对象原型操作'}
以下操作会触发代理对象的该拦截方法:
- Object.getPrototypeOf()
- Reflect.getPrototypeOf()
- __proto__
- Object.prototype.isPrototypeOf()
- instanceof
注意:
getPrototypeOf 方法必须返回一个对象或者 null。
3.8 handler.has
该拦截方法主要是针对 in 操作符的,in 操作符通常用来检测某个属性是否存在某个对象内。
语法:
let p8 = new Proxy(target, {
has: function (target, prop) {
}
});
参数解释:
- target:被代理的目标对象
- prop:需要检查是否存在的属性
以下操作可以触发该拦截函数:
- 属性查询:foo in proxy
- 继承属性查询:foo in Object.create(proxy)
- with 检查: with(proxy) { (foo); }
- Reflect.has()
使用案例:
let p8 = new Proxy({}, {
has: function (target, prop) {
console.log('检测的属性: ' + prop); // 检测的属性: a
return true;
}
});
console.log('a' in p8); // true
上段代码中我们代理对象是其实没有 a 属性,但是我们拦截之后直接返回的一个 true。
注意:
has 函数返回的必须是一个 Boolean 值。
3.9 handler.isExtensible
Object.isExtensible()方法主要是用来判断一个对象是否可以扩展,handler 中的 isExtensible 方法可以拦截该操作。
语法:
// 拦截 Object.isExtensible()
let p9 = new Proxy(target, {
isExtensible: function (target) {
}
});
参数解释:
- target:被代理的目标对象
使用案例:
let p9 = new Proxy({}, {
isExtensible: function (target) {
console.log('操作被拦截了');
return true;
}
});
console.log(Object.isExtensible(p9));
注意:
isExtensible 方法必须返回一个 Boolean 值或可转换成 Boolean 的值。
3.10 handler.ownKeys
静态方法 Reflect.ownKeys() 返回一个由目标对象自身的属性键组成的数组。handler 对象的 ownKeys 方法可以拦截该操作,除此之外,还有一些其它操作也会触发 ownKeys 操作。
语法:
let p10 = new Proxy(target, {
ownKeys: function (target) {
}
});
参数解释:
- target:被代理的目标对象
以下操作会触发拦截:
- Object.getOwnPropertyNames()
- Object.getOwnPropertySymbols()
- Object.keys()
- Reflect.ownKeys()
使用案例:
let p10 = new Proxy({}, {
ownKeys: function (target) {
console.log('被拦截了');
return ['a', 'b', 'c'];
}
});
console.log(Object.getOwnPropertyNames(p10)); // ['a', 'b', 'c']
注意:
ownKeys 的结果必须是一个数组,数组的元素类型要么是一个 String,要么是一个 Symbol。
3.11 handler.preventExtensions
Object.preventExtensions()方法让一个对象变的不可扩展,也就是永远不能再添加新的属性。handler.preventExtensions 可以拦截该项操作。
语法:
let p11 = new Proxy(target, {
preventExtensions: function (target) {
}
});
参数解释:
- target:被代理的目标对象
使用案例:
let p11 = new Proxy({}, {
preventExtensions: function (target) {
console.log('被拦截了');
Object.preventExtensions(target);
return true
}
});
Object.preventExtensions(p11);
以下操作会触发拦截:
- Object.preventExtensions()
- Reflect.preventExtensions()
注意:
如果目标对象是可扩展的,那么只能返回 false
3.12 handler.set
当我们给对象设置属性值时,将会触发该拦截。
语法:
let p12 = new Proxy(target, {
set: function (target, property, value, receiver) {
}
});
参数解释:
- target:被代理的目标对象
- property:将要被设置的属性名
- value:新的属性值
- receiver:最初被调用的对象,通常就是 proxy 对象本身
以下操作会触发拦截:
- 指定属性值:proxy[foo] = bar 和 proxy.foo = bar
- 指定继承者的属性值:Object.create(proxy)[foo] = bar
- Reflect.set()
使用案例:
let p12 = new Proxy({}, {
set: function (target, property, value, receiver) {
target[property] = value;
console.log('property set: ' + property + ' = ' + value); // property set: a = 10
return true;
}
});
p12.a = 10;
注意:
set() 方法应当返回一个布尔值
3.13 handler.setPrototypeOf
Object.setPrototypeOf() 方法设置一个指定的对象的原型,当调用该方法修改对象的原型时就会触发该拦截。
语法:
let p13 = new Proxy(target, {
setPrototypeOf: function (target, prototype) {
}
});
参数解释:
- target:被代理的目标对象
- prototype:对象新原型或者为 null
使用案例:
let p13 = new Proxy({}, {
setPrototypeOf: function (target, prototype) {
console.log("触发拦截"); // 触发拦截
return true;
}
});
Object.setPrototypeOf(p13, {name: '小猪课堂'})
注意:
如果成功修改了[[Prototype]], setPrototypeOf 方法返回 true,否则返回 false。
总结
很多小伙伴可能因为 Vue3 的原因知道了 Proxy 代理的存在,但是很多都只了解 set、get 等方法,其实 Proxy 提供了很多拦截供我们使用,具体在什么场景下使用什么拦截函数,还需要自己独立思考