没有前言,咱们直接单刀直入,直奔主题!
由于篇幅较长,为了不乱,我先写了一个本文的总体思路,如上图
首先从字面意思得知,Proxy
是代理的意思
那它是什么呢?通过typeof
来检测一下
console.log(typeof Proxy)//function
由上可以得知:
Proxy
是定义在window
上的全局变量- 它的类型是
function
并且首字母大写了,我们可以猜测,它并不是一个普通的function,它应该是一个构造函数或者说是一个类。
那来试试看,万一可以直接执行呢!
let proxy = Proxy()
结果让你失望了,以上代码会报错:Uncaught TypeError: Constructor Proxy requires 'new'
所以我们来试试正儿八经的试试new Proxy()
,如下:
let p = new Proxy()
以上代码执行也会抛出错误:Uncaught TypeError: Cannot create proxy with a non-object as target or handler
,意思是创建proxy
对象时,不能使用不是对象的东西作为target
或者handler
传入,什么意思呢?答案是要按照它的要求来传递参数
从以上报错信息,我们可以得出两个重要信息:
Proxy
在构造对象时接受两个参数:target
和handler
- 两个参数的类型必须是
object
那问题来了,这两个参数target
和handler
分别表示什么呢?
在最开始,我说过Proxy
的本意是代理意思,表示由它来“代理”某些操作;网上还有另外一种理解:
可以将
Proxy
理解成“拦截”,在目标对象之前架设一层“拦截”,当外界对该对象的访问,都必须先通过这层拦截,正因为有了一种拦截机制,当外界的访问我们可以对进行一些操作(过滤或改写)
所以我们可以很好的理解,target
表示的就是要拦截(代理)的目标对象;而handler
是用来定制拦截行为
target
很容易理解,关键就在handler
里头到底可以填什么呢?分别用于拦截对象的什么操作呢?
于是乎,我们猜测:handler中肯定存在与对象操作一一对应的方法?
那我们先回顾我们是怎么操作对象?为了方便,我这里列举出操作对象的所有方式
例如:js
let obj = {
name: 'alice',
showName() {
console.log(`my name is ${this.name}`)
}
}
- 获取对象属性
console.log(obj.name)//alice
- 给对象添加属性
obj.age = 12;
- 判断属性是否在对象中
console.log('age' in obj)//true
- 删除对象属性
delete obj.age
- 通过各种方法遍历对象的所有属性
console.log(Object.getOwnPropertyNames(obj));//["name", "showName"]
console.log(Object.getOwnPropertySymbols(obj));//[]
console.log(Object.keys(obj))//["name", "showName"]
for (let key in obj){
console.log(key)
}//分别打印name showName
- 获取对象的某个属性的描述对象
let d = Object.getOwnPropertyDescriptor(obj,'name')
console.log(d)
//{value: "alice", writable: true, enumerable: true, configurable: true}
- 使用Object身上的方法,为某个对象添加一个或多个属性
Object.defineProperty(obj,'age',{
value:12,
writable:true,
enumerable:true,
configurable:true
})
Object.defineProperties(obj,{
showAge:{
value:function(){console.log(`我今年${this.age}岁了`)},
writable:true,
enumerable:true,
configurable:true,
},
showInfo:{
value:function(){console.log(`我叫${this.name},我今年${this.age}岁了`)},
writable:true,
enumerable:true,
configurable:true,
}
})
- 获取一个对象的原型对象
Object.getPrototypeOf(obj)
console.log(Object.getPrototypeOf(obj) === obj.__proto__)//true
- 设置某个对象的原型属性对象
Object.setPrototypeOf(obj,null);
//表示设置对象的原型为null,也可以传入其他对象作为其原型
- 让一个对象变得不可扩展,即不能添加新的属性
Object.preventExtensions(obj)
- 查看一个对象是不是可扩展的
console.log(Object.isExtensible(obj));//false,因为上面设置了该对象为不可扩展对象
- 如果对象为function类型,function类型的对象可以执行被执行符号()以及.call()和.apply()执行
function fn(...args){
console.log(this,args)
}
fn(1,2,3);
fn.call(obj,1,2,3);
fn.apply(obj,[1,2,3]);
- 一切皆是对象。如果对象作为构造函数时,则该对象可以用new生成出新的对象
function Person(){}
let p1 = new Person();
以上都是对对象的一些操作!
那回到我们的new Proxy(target,handler)
中的handler
,我们之前说了,handler
是用于设置拦截行为的,其实拦截的内容就是上面这一系列的对象操作,当对象执行某个操作时,就会触发handler
里面定义的东西,而这些东西本质是一个个函数。
于是,我们对Proxy
有了比较全面的认知,知道它其实是构造函数,它可以构造出代理对象,这个代理对象可以代理目标对象target做一些事,当执行某个操作时,它会执行该操作所对应的函数。那问题来了,handler
中都有哪些函数呢?分别对应什么操作呢?
下面我们一个个来讲
get方法
get方法可自动接受3个参数target, propKey, receiver,分别表示要代理的目标对象、对象上的属性以及代理对象,该方法用于拦截某个属性的读取操作,比如proxy.foo
和proxy['foo']
var person = {
name: "Alice"
};
var proxy = new Proxy(person, {
get: function(target, propKey) {
if (propKey in target) {
return target[propKey];
} else {
throw new ReferenceError(`Prop name ${propKey} does not exist.`);
}
}
});
proxy.name // "Alice"
proxy.age // 抛出错误:Uncaught ReferenceError: Prop name age does not exist.
可以看到,上面我访问了不存在属性,正常情况下如果没有这个拦截函数,访问不存在的属性,只会返回undefined,这里由于被代理了,所以抛出错误了!
而且console.log(proxy === receiver)
返回true
set方法
set方法可自动接受4个参数:target, propKey, value, receiver,分别表示要代理的目标对象、对象上的属性、属性对应的值以及代理对象。
该方法用于拦截对象属性操作,像proxy.foo = xxx
或proxy['foo'] = xxx
,例如:
var person = {
name: "Alice"
};
var proxy = new Proxy(person, {
set(target, propKey, value, receiver) {
console.log(`设置 ${target} 的${propKey} 属性,值为${value}`);
target[propKey] = value
}
});
proxy.name = 'Tom'
proxy.age = 18
结果如下:
has方法
has方法接受target, propKey,用于拦截propKey in proxy
的操作,返回一个布尔值,表示属性是否存在。如下:
var person = {
name: "Alice"
};
var proxy = new Proxy(person, {
has(target, propKey) {
return propKey in target
}
});
if('name' in proxy){
console.log(proxy.name)
}
以上结果返回Alice
deleteProperty方法
可接收target, propKey,用于拦截delete操作,返回一个布尔值,表示是否删除成功。例如:
var person = {
name: "Alice"
};
var proxy = new Proxy(person, {
deleteProperty(target, propKey) {
return delete target[propKey]
}
});
console.log(delete proxy.name)//ture
console.log(proxy.name)//undefined
ownKeys方法
可接收target,用于拦截Object.getOwnPropertyNames(proxy)
、Object.getOwnPropertySymbols(proxy)
、Object.keys(proxy)
、for...in
循环等类似操作,返回一个数组,表示对象所拥有的keys,如下:
var person = {
name: "Alice"
};
var proxy = new Proxy(person, {
ownKeys(target) {
return Object.getOwnPropertyNames(target)//为了省事
}
});
console.log(Object.getOwnPropertyNames(proxy))
返回["name"]
getOwnPropertyDescriptor方法
接收target和propKey,用于拦截Object.getOwnPropertyDescriptor(proxy, propKey)
,返回属性的描述对象。如下:
var person = {
name: "Alice"
};
var proxy = new Proxy(person, {
getOwnPropertyDescriptor(target,propKey){
return Object.getOwnPropertyDescriptor(target, propKey)
}
});
console.log(Object.getOwnPropertyDescriptor(proxy, 'name'))
结果如下:
defineProperty方法
接收target, propKey, propDesc,分别表示目标对象、目标对象的属性,以及属性描述配置,用于拦截Object.defineProperty(proxy, propKey, propDesc)
和Object.defineProperties(proxy, propDescs)
的操作,例如:
var person = {};
var proxy = new Proxy(person, {
defineProperty(target,propKey,propKeypropDesc){
return Object.defineProperty(target, propKey, propKeypropDesc)
}
});
console.log(Object.defineProperty(proxy, 'name', {value:'Tom'}))
console.log(person.name)
preventExtensions方法
可接收target,用于拦截Object.preventExtensions(proxy)
操作,补充说明一下preventExtensions的作用是将一个对象变成不可扩展,也就是永远不能再添加新的属性。例如:
var person = {
name: "Alice"
};
var proxy = new Proxy(person, {
preventExtensions(target){
return Object.preventExtensions(target)
}
});
Object.preventExtensions(proxy)
proxy.age = 11;
console.log(person)
后面添加的age,并没有成功添加
getPrototypeOf(target)
在使用Object.getPrototypeOf(proxy)
会触发调用,返回一个对象
var person = {
name: "Alice"
};
var proxy = new Proxy(person, {
getPrototypeOf(target){
return Object.getPrototypeOf(target)
}
});
console.log(Object.getPrototypeOf(proxy))
isExtensible(target)
当使用Object.isExtensible(proxy)
时会触发调用,返回一个布尔值,表示是否可扩展,如下:
var person = {
name: "Alice"
};
var proxy = new Proxy(person, {
isExtensible(target){
return Object.isExtensible(target)
}
});
console.log(Object.isExtensible(proxy))//true
setPrototypeOf(target, proto)
当调用Object.setPrototypeOf(proxy, proto)
会触发该函数调用,例如:
var person = {
name: "Alice"
};
let proto = {}
let proxy = new Proxy(person,{
setPrototypeOf(target,proto){
console.log(`设置${target}的原型为${proto}`);
return Object.setPrototypeOf(target,proto)
}
});
Object.setPrototypeOf(proxy,proto)
console.log(Object.getPrototypeOf(person) === proto)
结果如下
apply(target, object, args)
接收三个参数target, object, args,分别表示目标对象、调用函数是的this指向以及参数列表,当Proxy
实例作为函数调用时触发,比如proxy(...args)
、proxy.call(object, ...args)
、proxy.apply(...)
,如下:
function f (x,y){ return x + y}
let proxy = new Proxy(f,{
apply(target, object, args){
console.log(`调用了f`);
return f.call(object,...args)
}
})
console.log(proxy(1,2));
结果如下:
construct(target, args)
接收target和args,表示目标函数即参数列表,当Proxy
实例作为构造函数时触发该函数调用,比如new proxy(...args)
,例如:
function F(){ }
let proxy = new Proxy(F,{
construct(target, args){
console.log(`调用了construct`);
return new target(...args)
}
})
console.log(new proxy())
结果如下:
以上这些方法都被称为捕获器,这些捕获器分别捕获对象不同的操作行为。
下面我们通过一个简单例子,来进一步搞清楚目标对象和代理对象之间的关系。
如下
var person = {
name: "Alice"
};
var proxy = new Proxy(person, {
set(target, propKey, value, receiver) {
console.log(`设置 ${target} 的${propKey} 属性,值为${value}`);
target[propKey] = value
}
});
proxy.age = 18
person.sex = 'female'
console.log(person,proxy)
结果如图
通过上述例子,我们可以得出以下4点结论:
- 代理对象不等于目标对象,他是目标对象的包装品
- 目标对象既可以直接操作,也可以被代理对象操作,且两者相互关联
- 如果直接操作目标对象,则会绕过代理定义的各种拦截行为
- 如果用了代理,那肯定是希望给对象的操作嵌入我们定义的特殊行为,所以一般就操作代理对象就好
如果你还模糊,那我来给你看看Proxy真面目。....也没啥好说的,其实就是一个构造函数,并且接受两个参数target和handler,返回代理对象
function Proxy(target,handler){
//...
}
这下彻底理解了Proxy,那我们来看看它的应用吧。由于代理模式是非常典型的编程模式,会在很多地方被应用,我们以Vue3数据响应式系统为例来简单讲讲!
Vue3定义了一系列的响应式API,比如reactive、ref等等,它们的特点是:当时数据发生变化时,页面会对应更新UI,而底层用的就是Proxy!我以reactive为例
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
return target[key]
},
set(target, key, val) {
target[key] = val
// 这里当数据变化时,更新界面,于是我们考虑到这里需要update方法用户更新
// 执行updata操作...
}
})
}
可以看到,数据对象obj通过reactive包装成了代理对象,当数据发生变化时,会调用set方法,在更新数据的同时,同时执行一些update的操作
这就是典型的代理模式应用~
到这里应该没有什么问题了吧,有问题欢迎下方留言告知,谢谢!
END~