写点什么

一文带你深入掌握 ES6 Proxy 数据代理

  • 2022 年 9 月 12 日
    河南
  • 本文字数:5289 字

    阅读完需:约 17 分钟

一文带你深入掌握ES6 Proxy数据代理

前言

ES6之前,我们常使用Object.defineProperty()方法来进行数据代理从而实现数据的劫持(如:Vue2的响应式原理


而在ES6之后诞生了一个全新的对象(构造器):Proxy,作为数据代理而言,它比Object.defineProperty()要强大许多,这也是为什么Vue3的响应式要使用Proxy来做的原因


这篇文章将深入去研究Proxy代理,让我们开始吧!

一、为什么要使用代理?

之所以使用代理,就是不希望用户能够直接访问某个对象,直接操作对象的某个成员(因为这样是不可控的,我们不知道用户在访问操作哪一个对象)


通过代理,我们可以拦截用户的访问(称为数据劫持),拦截住后我们就可以对数据进行一些处理,比如做一些数据的验证或者像 Vue 一样做一些视图更新的额外操作,之后再允许用户的访问操作(因为我们拦截了用户的每一次访问,这样用户操作对象就完全是在我们可控的范围内)


简单来说,就是我们希望用户在访问对象时我们能够清除的知道用户在访问什么并且能够在中间做一些我们自己的操作

二、Proxy 是什么?

Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)


ProxyES6 中新增的一个构造函数,也可以叫类,通过new操作符调用使用。但在JavaScript中函数和类本质上也是对象,所以我们也能将Proxy 直接作为对象访问它的属性进行操作,如Proxy.revocable()


但需要注意的是,Proxy并没有prototype原型对象:


 console.log(Proxy.prototype); // undefined
复制代码


ECMA 的官方说明: 因为 Proxy 构造出来的实例对象仅仅是对目标对象的一个代理,所以 Proxy 在构造过程中是不需要 prototype 进行初始化的


其他构造函数之所以需要 prototype,是因为构造出来的对象需要一些初始化的成员,所以将这些成员定义到了 protoype

三、基础语法

const proxyTarget = new Proxy(target, handler)
复制代码


👉 参数:


  • target:Proxy 会对 target 对象进行包装。它可以是任何类型的对象,包括内置的数组,函数甚至是另一个代理对象。

  • handler:它是一个对象,它的属性提供了某些操作发生时所对应的处理函数

  • 一个空的 handler 参数将会创建一个与被代理对象行为几乎完全相同的代理对象。通过在 handler 对象上定义一组处理函数,你可以自定义被代理对象的一些特定行为。例如, 通过定义 get() 你就可以自定义被代理对象的 属性访问器


👉 返回值:


  • proxyTarget :经过 Proxy 包装后的 target 对象


👉 基础使用:


const obj = {    name: 'Ailjx'}const proxyTarget = new Proxy(obj, {})console.log(proxyTarget);
复制代码



需要注意的是,返回值proxyTarget 并不是target的深拷贝,而只是浅引用:


const obj = {    name: 'Ailjx'}const newObj = new Proxy(obj, {})obj.name = 9 // 修改obj,newObj也会改变console.log(newObj.name); // 9
复制代码


const obj = {    name: 'Ailjx'}const newObj = new Proxy(obj, {})newObj.name = 9 // 修改newObj,obj也会改变console.log(obj.name); // 9
复制代码

四、handler 处理函数

Proxy代理的灵魂就在于它的第二个参数:handler对象,在这个对象内我们可以定义一些处理函数来进行数据劫持,从而实现一些额外的操作

🎉 apply() 拦截函数的调用

handler.apply() 方法用于拦截函数的调用


👉 语法:


var ProxyTarget = new Proxy(target, {  apply: function(target, thisArg, argumentsList) {}  // 或  // apply(target, thisArg, argumentsList) {}  // 或  // apply: (target, thisArg, argumentsList) => {},});
复制代码


👉 参数:


下面的参数将会传递给 apply() 方法,this 绑定在 handler


  • target:目标对象(函数)

  • thisArg:被调用时的上下文对象

  • argumentsList:被调用时的参数数组


👉 返回值:


apply方法可以返回任何值


👉 使用:


function sum(a, b) {    return a + b;}
const handler = { // target:目标对象(函数) // thisArg:被调用时的上下文对象 // argumentsList:被调用时的参数数组 apply: function (target, thisArg, argumentsList) { console.log(`你调用了函数!`); return target(argumentsList[0], argumentsList[1]) * 10; }, // 或者这样写 // apply(target, thisArg, argumentsList) { // console.log(`你调用了函数!`); // return target(argumentsList[0], argumentsList[1]) * 10; // },};// Proxy的第一个参数可以是任意类型的对象,此处为函数const newSum = new Proxy(sum, handler);
console.log(sum(1, 2)); // 3console.log(newSum(1, 2)); // 你调用了函数! 30
复制代码

🎉 construct() 拦截 new 操作符

handler.construct() 方法用于拦截 new 操作符。为了使 new 操作符在生成的 Proxy 对象上生效,用于初始化代理的目标对象自身必须具有 [[Construct]] 内部方法(即 new target 必须是有效的)


👉 语法:


var ProxyTarget = new Proxy(target, {  construct: function(target, argumentsList, newTarget) {}});
复制代码


👉 参数:


下面的参数将会传递给 construct 方法,this 绑定在 handler


  • target:目标对象。

  • argumentsList:constructor 的参数列表。

  • newTarget最初被调用的构造函数,就上面的例子而言是 newTarget


👉 返回值:


construct 方法必须返回一个对象


👉 使用:


function MyName(name) {    this.name = name;}
const handler = { construct(target, args, newTarget) { console.log(`你使用了new操作符!`, newTarget); return new target(...args); },};
const ProxyMyName = new Proxy(MyName, handler);
console.log(new ProxyMyName("Ailjx").name);
复制代码


🎉 get() 拦截对象属性的读取操作

handler.get() 方法用于拦截对象的读取属性操作


👉 语法:


var proxyTarget = new Proxy(target, {  get: function(target, property, receiver) {}});
复制代码


👉 参数:


以下是传递给 get 方法的参数,this 上下文绑定在handler 对象上


  • target:目标对象。

  • property:被获取的属性名。

  • receiver:Proxy 或者继承 Proxy 的对象


👉 返回值:


get 方法可以返回任何值,这些返回值就是用户真正获取到的属性值


👉 使用:


const obj = {    name: "Ailjx",};
var p = new Proxy(obj, { get: function (target, property, receiver) { console.log("你访问的属性为:", property); console.log(receiver); return "My name is " + target.name; },});
console.log(p.name);
复制代码


🎉 set() 拦截对象属性的修改/设置操作

handler.set() 方法是设置属性值操作的捕获器


👉 语法:


const p = new Proxy(target, {  set: function(target, property, value, receiver) {}});
复制代码


👉 参数:


下面的参数将会传递给 set() 方法,this 绑定在 handler


  • target:目标对象

  • property:将被设置的属性名或 Symbol。

  • value:新属性值。

  • receiver:最初被调用的对象。通常是 proxy 本身,但 handlerset 方法也有可能在原型链上,或以其他方式被间接地调用(因此不一定是 proxy 本身)

  • 假设有一段代码执行 obj.name = "Ailjx"obj 不是一个 proxy,且自身不含 name 属性,但是它的原型链上有一个 proxy,那么,那个 proxyset() 处理器会被调用,而此时,obj 会作为 receiver 参数传进来


👉 返回值:


set() 方法应当返回一个布尔值


  • 返回 true 代表属性设置成功

  • 在严格模式下,如果 set() 方法返回 false,那么会抛出一个 TypeError 异常


👉 使用:


const obj = {    name: "Ailjx",};
const handler = { set(target, property, value) { if (property === "name" && typeof value !== "string") { console.log("姓名必须是字符串!"); } else { target.name = value; return true; } },};
const proxy1 = new Proxy(obj, handler);
proxy1.name = 1;console.log(proxy1.name);
proxy1.name = "Chen";console.log(proxy1.name);
复制代码


🎉 deleteProperty()拦截对象属性的删除操作

handler.deleteProperty() 方法用于拦截对对象属性的删除操作


👉 语法:


var p = new Proxy(target, {  deleteProperty: function(target, property) {}});
复制代码


👉 参数:


deleteProperty方法将会接受以下参数。this 被绑定在 handler


  • target:目标对象

  • property:待删除的属性名。


👉 返回值:


deleteProperty 必须返回一个 Boolean 值,表示了该属性是否被成功删除


👉 使用:


const obj = {    name: "Ailjx",};var p = new Proxy(obj, {    deleteProperty: function (target, prop) {        delete target[prop];        console.log("你删除了" + prop + "属性");        return true;    },});console.log(p.name); // Ailjxdelete p.name; // 你删除了name属性console.log(p.name); // undefined
复制代码


🎉 has() 拦截 in 操作符

handler.has() 方法是针对 in 操作符的代理方法


👉 语法:


var p = new Proxy(target, {  has: function(target, prop) {}});
复制代码


👉 参数:


下面的参数将会传递给 has() 方法,this 绑定在 handler


  • target:目标对象

  • prop:需要检查是否存在的属性的名称


👉 返回值:


has 方法返回一个 boolean


👉 使用:


const handler = {    has(target, prop) {        if (prop[0] === "_") {            console.log("不允许判断_开头的属性");            return false;        }        if (prop in target) {            console.log("✅你判断的属性存在!");            return true;        } else {            console.log("❌你判断的属性不存在!");            return false;        }    },};
const obj = { _name: "艾莉加薪", name: "Ailjx", age: 18,};
const proxyObj = new Proxy(obj, handler);
console.log("aaa" in proxyObj); // falseconsole.log("----------------------");
console.log("_name" in proxyObj); // falseconsole.log("_name" in obj); // trueconsole.log("----------------------");
console.log("name" in proxyObj); // true
复制代码


🎉 更多处理函数

handler对象内还有以下处理函数:


五、可撤销代理

在前面说过:能将Proxy 直接作为对象访问它的属性进行操作,如Proxy.revocable()


这个Proxy.revocable() 方法可以用来创建一个可撤销的代理对象,先看下面这个例子:


// 创建可撤销的代理const revocable = Proxy.revocable({},{        get(target, name) {            return "[[" + name + "]]";        },    });
console.log(revocable);
复制代码



Proxy.revocable() 方法具有和Proxy一样的两个参数:target目标对象和handler对象


但它的返回值有点特殊,它返回一个包含了代理对象本身和它的撤销方法的可撤销 Proxy 对象,其结构为:


 {"proxy": proxy, "revoke": revoke}
复制代码


  • proxy:表示新生成的代理对象本身,和用一般方式 new Proxy(target, handler) 创建的代理对象没什么不同,只是它可以被撤销掉

  • revoke:撤销方法,调用的时候不需要加任何参数,就可以撤销掉和它一起生成的那个代理对象

  • 一旦某个代理对象被撤销,它将变得几乎完全不可调用,在它身上执行任何的可代理操作都会抛出 TypeError 异常(可代理操作指的就是我们在handler对象函数属性上能拦截到的操作,一共有 14 种,执行这 14 种以外的情况不会报错)

  • 一旦被撤销,这个代理对象便不可能被直接恢复到原来的状态,同时和它关联的目标对象以及处理器对象都有可能被垃圾回收掉。再次调用撤销方法 revoke() 则不会有任何效果,但也不会报错


👉 示例:


// 创建可撤销的代理const revocable = Proxy.revocable(    {},    {        get(target, name) {            return "[[" + name + "]]";        },    });

// 获取创建的代理const proxy = revocable.proxy;proxy.foo; // "[[foo]]"
// 撤销代理revocable.revoke();// 代理撤销后,可代理操作将不能再被使用console.log(proxy.foo); // 抛出 TypeErrorproxy.foo = 1; // 还是 TypeErrordelete proxy.foo; // 又是 TypeError
typeof proxy; // "object",因为 typeof 不属于可代理操作
复制代码

结语

深入了解了Proxy之后,真的会被它强大的代理拦截功能所折服,在它的基础上我们可以创建几乎任何我们想要的响应式系统,它像是一个硕大的地基,至于地基之上需要建筑什么,全由我们自己掌握!


看完本篇文章相信你已经对Proxy有了深入的理解,学习Proxy是我们学习像Vue3这种响应式原理的第一步,大家加油!


如果本篇文章对你有所帮助,还请客官一件四连!❤️

发布于: 刚刚阅读数: 5
用户头像

前端之行,任重道远! 2022.08.25 加入

本科大三学生、CSDN前端领域新星创作者、华为云享专家、第十三届蓝桥杯国赛三等奖获得者

评论

发布
暂无评论
一文带你深入掌握ES6 Proxy数据代理_JavaScript_海底烧烤店ai_InfoQ写作社区