1.基本概念
想要学习一个新的 API
或者知识,不能一上来就看它怎么使用。我们要学习从基本概念入手,这样才能做到有始有终。
Proxy
是在 ES6
中才被标准化的,而 Vue2.x
版本是基于 ES6
版本之前的 Object.defineProperty()
设计的,我们先来看下官方是怎么解释 Proxy
的。
官网解释:
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
就是一个构造函数,只不过是代理之后的新函数,我们使用 k
操作符实例化它的,首先就会去执行 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
中的 getPrototypeOf
方法。
语法:
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
提供了很多拦截供我们使用,具体在什么场景下使用什么拦截函数,还需要自己独立思考。
如果觉得文章太繁琐或者没看懂,可以观看视频: 小猪课堂