一篇彻底理解Proxy

12,954 阅读8分钟

没有前言,咱们直接单刀直入,直奔主题!

由于篇幅较长,为了不乱,我先写了一个本文的总体思路,如上图

首先从字面意思得知,Proxy是代理的意思

那它是什么呢?通过typeof 来检测一下

console.log(typeof Proxy)//function

由上可以得知:

  1. Proxy是定义在window上的全局变量
  2. 它的类型是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传入,什么意思呢?答案是要按照它的要求来传递参数

从以上报错信息,我们可以得出两个重要信息:

  1. Proxy在构造对象时接受两个参数:targethandler
  2. 两个参数的类型必须是object

那问题来了,这两个参数targethandler分别表示什么呢?

在最开始,我说过Proxy 的本意是代理意思,表示由它来“代理”某些操作;网上还有另外一种理解:

可以将Proxy理解成“拦截”,在目标对象之前架设一层“拦截”,当外界对该对象的访问,都必须先通过这层拦截,正因为有了一种拦截机制,当外界的访问我们可以对进行一些操作(过滤或改写)

所以我们可以很好的理解,target表示的就是要拦截(代理)的目标对象;而handler是用来定制拦截行为

target很容易理解,关键就在handler里头到底可以填什么呢?分别用于拦截对象的什么操作呢?

于是乎,我们猜测:handler中肯定存在与对象操作一一对应的方法?

那我们先回顾我们是怎么操作对象?为了方便,我这里列举出操作对象的所有方式

例如:js

let obj = {
	name: 'alice',
	showName() {
		console.log(`my name is ${this.name}`)
	}
}
  1. 获取对象属性
console.log(obj.name)//alice
  1. 给对象添加属性
obj.age = 12;
  1. 判断属性是否在对象中
console.log('age' in obj)//true
  1. 删除对象属性
delete obj.age
  1. 通过各种方法遍历对象的所有属性
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
  1. 获取对象的某个属性的描述对象
let d = Object.getOwnPropertyDescriptor(obj,'name')
console.log(d)
//{value: "alice", writable: true, enumerable: true, configurable: true}
  1. 使用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,
	}	
})
  1. 获取一个对象的原型对象
Object.getPrototypeOf(obj)		
console.log(Object.getPrototypeOf(obj) === obj.__proto__)//true
  1. 设置某个对象的原型属性对象
Object.setPrototypeOf(obj,null);
//表示设置对象的原型为null,也可以传入其他对象作为其原型
  1. 让一个对象变得不可扩展,即不能添加新的属性
Object.preventExtensions(obj)
  1. 查看一个对象是不是可扩展的
console.log(Object.isExtensible(obj));//false,因为上面设置了该对象为不可扩展对象
  1. 如果对象为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]);
  1. 一切皆是对象。如果对象作为构造函数时,则该对象可以用new生成出新的对象
function Person(){}
let p1 = new Person();

以上都是对对象的一些操作!

那回到我们的new Proxy(target,handler)中的handler,我们之前说了,handler是用于设置拦截行为的,其实拦截的内容就是上面这一系列的对象操作,当对象执行某个操作时,就会触发handler里面定义的东西,而这些东西本质是一个个函数。

于是,我们对Proxy有了比较全面的认知,知道它其实是构造函数,它可以构造出代理对象,这个代理对象可以代理目标对象target做一些事,当执行某个操作时,它会执行该操作所对应的函数。那问题来了,handler中都有哪些函数呢?分别对应什么操作呢?

下面我们一个个来讲

  1. get方法

get方法可自动接受3个参数target, propKey, receiver,分别表示要代理的目标对象、对象上的属性以及代理对象,该方法用于拦截某个属性的读取操作,比如proxy.fooproxy['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

  1. set方法

set方法可自动接受4个参数:target, propKey, value, receiver,分别表示要代理的目标对象、对象上的属性、属性对应的值以及代理对象。

该方法用于拦截对象属性操作,像proxy.foo = xxxproxy['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

结果如下:

image.png

  1. 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

  1. 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
  1. 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"]

  1. 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'))

结果如下:

image.png

  1. 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)
  1. 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,并没有成功添加

image.png

  1. 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))
  1. 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
  1. 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)

结果如下

image.png

  1. 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));

结果如下:

image.png

  1. 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())

结果如下:

image.png

以上这些方法都被称为捕获器,这些捕获器分别捕获对象不同的操作行为。

下面我们通过一个简单例子,来进一步搞清楚目标对象和代理对象之间的关系。

如下

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)

结果如图

image.png

通过上述例子,我们可以得出以下4点结论:

  1. 代理对象不等于目标对象,他是目标对象的包装品
  2. 目标对象既可以直接操作,也可以被代理对象操作,且两者相互关联
  3. 如果直接操作目标对象,则会绕过代理定义的各种拦截行为
  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~