# 前言

就目前而言,前端框架基本上被 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>

输出结果:

img

# 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] = barproxy.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 提供了很多拦截供我们使用,具体在什么场景下使用什么拦截函数,还需要自己独立思考。