前言
就目前而言,前端框架基本上被 Vue 和 React 瓜分得差不多了。如果你去面试一个前端岗位,那么或多或少都会问你一些关于 Vue 和 React 框架得知识,无论是原理还是使用,我们都有必要去了解一番。
数据响应式可以说是这些框架的一大特色与核心,这里我们就拿 Vue 来说。在 Vue2.x 时代,实现数据响应式主要是使用
Object.defineProperty()
这个 API 来实现的,而到了 Vue3.x 时代,数据响应式主要是使用Proxy()
来实现的。如果你还不了解 Proxy,那么你很有必要跟着本篇文章学习一下!
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 就是一个构造函数,只不过是代理之后的新函数,我们使用 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 中的 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 提供了很多拦截供我们使用,具体在什么场景下使用什么拦截函数,还需要自己独立思考。