你对JS中的Proxy代理了解多少?

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


输出结果:



42.png


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:想要获取的属性名
  • receiverProxy 或者继承 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 必须返回一个 objectundefined
  • 使用 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 代理的存在,但是很多都只了解 setget 等方法,其实 Proxy 提供了很多拦截供我们使用,具体在什么场景下使用什么拦截函数,还需要自己独立思考。


如果觉得文章太繁琐或者没看懂,可以观看视频: 小猪课堂



相关文章
|
4天前
|
缓存 JavaScript 数据安全/隐私保护
js开发:请解释什么是ES6的Proxy,以及它的用途。
`ES6`的`Proxy`对象用于创建一个代理,能拦截并自定义目标对象的访问和操作,应用于数据绑定、访问控制、函数调用的拦截与修改以及异步操作处理。
26 3
|
4天前
|
API
在vite.config.js 配置代理
在vite.config.js 配置代理
98 2
|
4天前
|
JavaScript 前端开发 定位技术
JavaScript 中如何代理 Set(集合) 和 Map(映射)
JavaScript 中如何代理 Set(集合) 和 Map(映射)
53 0
|
4天前
|
JSON JavaScript 前端开发
Webpack【Webpack图片处理、Webpack中proxy代理 、自动清理dist、Webpack优化、JavaScript中的代码检查】(三)-全面详解(学习总结---从入门到深化)(下)
Webpack【Webpack图片处理、Webpack中proxy代理 、自动清理dist、Webpack优化、JavaScript中的代码检查】(三)-全面详解(学习总结---从入门到深化)
53 2
|
6月前
|
缓存 JavaScript 应用服务中间件
Nginx+Tomcat代理环境下JS无法完全加载问题
Nginx+Tomcat代理环境下JS无法完全加载问题
|
4天前
|
JSON JavaScript 前端开发
vue2_vite.config.js的proxy跨域配置和nginx配置代理有啥区别?
vue2_vite.config.js的proxy跨域配置和nginx配置代理有啥区别?
34 1
|
4天前
|
JavaScript 前端开发 API
Python之JavaScript逆向系列——通过IP代理高频获取全篇小说
Python之JavaScript逆向系列——通过IP代理高频获取全篇小说
40 0
|
4天前
|
JSON 前端开发 JavaScript
Webpack【Webpack图片处理、Webpack中proxy代理 、自动清理dist、Webpack优化、JavaScript中的代码检查】(三)-全面详解(学习总结---从入门到深化)
Webpack【Webpack图片处理、Webpack中proxy代理 、自动清理dist、Webpack优化、JavaScript中的代码检查】(三)-全面详解(学习总结---从入门到深化)
41 0
Webpack【Webpack图片处理、Webpack中proxy代理 、自动清理dist、Webpack优化、JavaScript中的代码检查】(三)-全面详解(学习总结---从入门到深化)
|
4天前
|
缓存 前端开发 JavaScript
理解 Proxy 和 Object.defineProperty:提升你的 JavaScript 技能(下)
理解 Proxy 和 Object.defineProperty:提升你的 JavaScript 技能(下)
理解 Proxy 和 Object.defineProperty:提升你的 JavaScript 技能(下)
|
4天前
|
JavaScript 前端开发 测试技术
理解 Proxy 和 Object.defineProperty:提升你的 JavaScript 技能(上)
理解 Proxy 和 Object.defineProperty:提升你的 JavaScript 技能(上)
理解 Proxy 和 Object.defineProperty:提升你的 JavaScript 技能(上)