彻底理解Proxy和Reflect

Proxy

ProxyES6 引入的一种用于定义自定义行为的对象包装器。通过 Proxy,你可以拦截和重写对目标对象的基本操作,例如读取属性、设置属性、删除属性、函数调用等

引入

首先来看一个需求:有一个对象,希望监听这个对象中的属性被设置或获取的过程

  • 可以通过之前的属性描述符中的存储属性描述符来做到
const obj = {
  name: "obj",
  age: 18,
};
Object.keys(obj).forEach((f) => {
  let v = obj[f];
  Object.defineProperty(obj, f, {
    set(value) {
      console.log("set");
      v = value;
    },
    get() {
      console.log("get");
      return v;
    },
  });
});
obj.height = 188; // 监听不到
obj.age = 28; // 执行set
console.log(obj.age); // 执行get  28

这样做有什么缺点呢?

  • 首先 Object.defineProperty 设计的初衷不是为了去监听截止一个对象中所有的属性的,初衷其实是定义普通的属性

  • 其次如果我们想监听更加丰富的操作,比如新增属性、删除属性,那么 Object.defineProperty 是无能为力的

因此,在ES6中,新增了一个Proxy类,这个类从名字就可以看出来,是用于帮助我们创建一个代理的

  • 也就是说,如果我们希望监听一个对象的相关操作,那么可以先创建一个代理对象(Proxy对象)
  • 之后对该对象的所有操作,都通过代理对象来完成,代理对象可以监听我们想要对原对象进行哪些操作

基本概念

const p = new Proxy(target, handler)

  • 代理对象 (proxy) :通过 Proxy 创建的代理对象(上面代码的p),操作代理对象时,行为由处理器决定

  • 目标对象 (target) :你想要代理的对象,可以是任何类型的对象(数组、函数、对象等)

  • 处理器 (handler) :定义拦截操作的对象,包含一组陷阱函数(比如 getsethasdeleteProperty 等等)

const obj = {
  name: "obj",
  age: 18,
};
const p = new Proxy(obj, {
  // 这个handler对象中放捕获器
  set() {
    console.log("set捕获器");
  },
});

p.name = "p"; // 这时会打印set捕获器

拦截器使用

如果我们想要侦听某些具体的操作,那么就可以在handler中添加对应的捕捉器(Trap),总共有13个捕获器

setget

setget分别对应的是函数类型

  • set函数有四个参数:拦截属性设置

    • handler.set(target,property,value,receiver)
    • target:目标对象(侦听的对象);
    • property:将被设置的属性key
    • value:新属性值;
    • receiver:调用的代理对象
  • get函数有三个参数:拦截属性读取

    • handler.get(target,property,receiver)
    • target:目标对象(侦听的对象);
    • property:被获取的属性key
    • receiver:调用的代理对象
const obj = {
  name: "obj",
  age: 18,
};
const p = new Proxy(obj, {
set(target, key, value, receiver) {
  // {name: 'obj', age: 18}  name   p   Proxy(Object){name: 'obj', age: 18}
    console.log("set捕获器", target, key, value, receiver);
    target[key] = value; // 不写赋值为undefined
  },
  get(target, key, receiver) {
    console.log("get捕获器");
    return target[key]; // 不写获取值为undefined
  },
});
p.name = "p"; // 这时会打印set捕获器
console.log(p.name); // 这时会打印get捕获器
p.height = 188; // 可捕获到
console.log(p.height); 

hasdeleteProperty

  • has(target, property)in 操作符的捕捉器,拦截 in 操作符,检查属性是否存在

    • target: 被代理的对象
    • property: 被检查的属性名
  • deleteProperty(target, property):拦截 delete 操作符删除属性

    • target: 被代理的对象
    • property: 被检查的属性名
const obj = {
  name: "obj",
  age: 18,
};
const p = new Proxy(obj, {
  has(target, key) {
    console.log("has捕获器");
    return key in target; // 不写都返回false
  },
  deleteProperty(target, key) {
    console.log("deleteProperty捕获器");
    delete target[key];
  },
});
console.log("school" in p); // false
console.log("age" in p); // true
delete p.age;
console.log(p); // Proxy(Object) {name: 'p'}

applyconstruct

  • apply(target, thisArg, argumentsList):拦截函数调用操作

    • target: 被代理的函数
    • thisArg: 函数的 this
    • argumentsList: 函数调用时传入的参数列表
  • construct(target, argumentsList, newTarget)new 操作符的捕捉器

    • target: 被代理的构造函数
    • argumentsList: 构造函数调用时传入的参数列表
    • newTarget: new 操作符的目标
const Foo = function (name) {
  this.name = name;
  console.log("foo");
};
const p = new Proxy(Foo, {
  apply(target, thisArg, args) {
    console.log("apply捕获器", target, thisArg, args);
    return target.apply(thisArg, args); // 不写不会执行函数调用
  },
  construct(target, args, newTarget) {
    console.log("construct捕获器", target, args, newTarget);
    return new target(...args);
  },
});
p(); // apply捕获器执行
const f = new p("construct捕获器");
console.log(f); // Foo {name: 'construct捕获器'}

剩余的7个拦截器

  • setPrototypeOf(target, prototype):拦截 Object.setPrototypeOf() 操作

    • target: 被代理的对象
    • prototype: 新的原型对象
  • getPrototypeOf(target):拦截 Object.getPrototypeOf() 操作

    • target: 被代理的对象
  • isExtensible(target):拦截对象是否可以添加新属性,即 Object.isExtensible() 操作,

    • target: 被代理的对象
  • preventExtensions(target):拦截阻止向对象添加新属性,仍然可以将属性添加到对象原型中,即 Object.preventExtensions() 操作,

    • target: 被代理的对象
  • getOwnPropertyDescriptor(target, property):拦截对属性描述符的请求,即 Object.getOwnPropertyDescriptor()

    • target: 被代理的对象
    • property: 请求的属性名
  • defineProperty(target, property, descriptor):拦截在对象上定义新属性,或者修改对象上现有的属性,并返回该对象,即 Object.defineProperty() 操作

    • target: 被代理的对象
    • property: 被定义的属性名
    • descriptor: 属性描述符对象
  • ownKeys(target):拦截对象的自有属性键请求,如 Object.keys()for...in

    • target: 被代理的对象
const obj = {
  name: "obj",
  age: 18,
  running() {
    console.log("running");
  },
};
const proxy = new Proxy(obj, {
  setPrototypeOf(target, prototype) {
    console.log("setPrototypeOf捕获器", target, prototype);
    Object.setPrototypeOf(target, prototype);
    return true;
  },
  getPrototypeOf(target) {
    console.log("getPrototypeOf捕获器", target);
    return Object.getPrototypeOf(target);
  },
  isExtensible(target) {
    console.log("isExtensible捕获器", target);
    return Object.isExtensible(target);
  },
  preventExtensions(target) {
    console.log("preventExtensions捕获器", target);
    return Object.preventExtensions(target);
  },
  getOwnPropertyDescriptor(target, prototype) {
    console.log("getOwnPropertyDescriptor捕获器", target, prototype);
    return Object.getOwnPropertyDescriptor(target, prototype);
  },
  defineProperty(target, prototype, description) {
    console.log("defineProperty捕获器", target, prototype, description);
    return Object.defineProperty(target, prototype, description);
  },
  ownKeys(target) {
    console.log("ownKeys捕获器", target);
    return Object.keys(target);
  },
});
Object.setPrototypeOf(proxy, {});
console.log(Object.getPrototypeOf(proxy)); // {}
console.log(Object.isExtensible(proxy));
Object.preventExtensions(proxy); // 不能添加新属性
console.log(Object.getOwnPropertyDescriptor(proxy, "name")); // {value: 'obj', writable: true, enumerable: true, configurable: true}
console.log(Object.keys(proxy)); // ['name', 'age', 'running']
Object.defineProperty(proxy, "height", {
  value: 188,
}); // 因为设置了preventExtensions,所以报错:Cannot define property height, object is not extensible

代理对象和原对象

  • 当你使用 Proxy 包装一个目标对象时,返回的是一个新的代理对象,这个代理对象并不是目标对象的引用,代理对象是独立的对象

  • 但它通过内部机制与目标对象相关联。你可以在代理对象上进行各种操作(如读取属性、设置属性、删除属性等)

它们是怎么建立关联的呐?

  • JavaScript 引擎内部,每个代理对象都有两个重要的内部槽位[[Target]][[Handler]],打印代理对象时在浏览器可以看到

    • [[Target]] :指向目标对象
    • [[Handler]] :指向处理程序对象
  • 槽位保持代理对象和目标对象之间的关联。JavaScript 语言规范规定了这些槽位的存在,虽然我们不能在代码中直接访问它们,但它们是代理行为的核心

Reflect

ReflectES6 引入的一个内置对象,它与 Proxy 一起使用,但也可以单独使用。Reflect 提供了一系列与对象操作相关的方法

引入

Reflect也是ES6新增的一个API,它是一个对象,字面的意思是反射

那么这个Reflect有什么用呢?

  • 它主要提供了很多操作JavaScript对象的方法,有点像Object中操作对象的方法

  • 比如 Reflect.getPrototypeOf(target) 类似于 Object.getPrototypeOf()

  • 比如 Reflect.defineProperty(target, propertyKey, attributes) 类似于Object.defineProperty()

以前有Object可以做这些操作,为什么还需要有Reflect这样的新增对象呢?

  • 为了标准化对象操作的接口

    • 因为在早期的ECMA规范中没有考虑到对对象本身的操作如何设计会更加规范,所以将这些API放到了Object上面

    • 但是Object作为一个构造函数,这些操作实际上放到它身上并不合适

  • 为了改善函数行为的一致性

    • 一些对象操作,比如 deletein,在错误的情况下不会抛出错误,而是静默失败。通过 Reflect,这些操作会返回布尔值来反映操作是否成功,这让代码更具可预测性

    • 所以在ES6中新增了Reflect,让我们这些操作都集中到了Reflect对象上

  • 其他作用学习ProxyReflect配合使用时讲解

常见方法使用

Reflect中常见的方法和Proxy是一一对应的,也是13个

setget

  • Reflect.set(target, propertyKey, value, receiver):为对象的某个属性设置值,类似于 obj[key] = value 的操作,返回一个Boolean,如果更新成功,则返回true

    • target:要修改属性的对象
    • propertyKey:属性的键名
    • value:要设置的值
    • receiver (可选):用于设置代理对象上的属性,结合Proxy使用中讲解
  • Reflect.get(target, propertyKey, receiver):获取对象身上某个属性的值,类似于 target[name]

    • target:要获取属性的对象
    • propertyKey:属性的键名
    • receiver (可选):若为 Proxy,则为代理对象的 this,结合Proxy使用中讲解
const obj = {
  name: "obj",
  age: 18,
};
console.log(Reflect.set(obj, "height", "188")); // true
console.log(Reflect.get(obj, "height")); // 188

hasdeleteProperty

  • Reflect.has(target, propertyKey):类似于 in 运算符,检查对象是否具有某个属性

    • target:要检查的对象
    • propertyKey:属性的键名
  • Reflect.deleteProperty(target, propertyKey):类似于 delete 运算符,删除对象上的属性,成功返回true

    • target:要删除属性的对象
    • propertyKey:属性的键名
const obj = {
  name: "obj",
  age: 18,
};
console.log(Reflect.has(obj, "age")); // true
console.log(Reflect.deleteProperty(obj, "name")); // true
console.log(obj); // {age: 18}

applyconstruct

  • Reflect.apply(target, thisArgument, argumentsList):对一个函数进行调用操作,同时可以传入一个数组作为调用参数,和 Function.prototype.apply() 功能类似

    • target:要调用的函数
    • thisArgument:调用时作为 this 的值
    • argumentsList:传递给目标函数的参数数组
  • Reflect.construct(target, argumentsList, newTarget):对构造函数进行 new 操作,相当于执行 new target(...args)

    • target:构造函数
    • argumentsList:传递给构造函数的参数数组
    • newTarget (可选):指定构造函数的新 newTarget(用于继承场景)
function Person() { }
function Student(n) {
  this.name = n;
  console.log("Student", n, this); // Student  name  String{'this'}
};
Reflect.apply(Student, "this", ["name"]);
const stu = Reflect.construct(Student, ["condtruct"], Person)
console.log(stu); // Student {name: 'condtruct'}
console.log(stu.__proto__ === Person.prototype) // true

剩余的7个方法

  • Reflect.setPrototypeOf(target, prototype):类似于 Object.setPrototypeOf,设置对象的原型,返回一个 Boolean, 如果更新成功,则返回 true

    • target:要设置原型的对象
    • prototype:新的原型对象
  • Reflect.getPrototypeOf(target):类似于 Object.getPrototypeOf,获取对象的原型

    • target:要获取原型的对象
  • Reflect.isExtensible(target):类似于 Object.isExtensible,检查对象是否可扩展,返回一个Boolean

    • target:要检查的对象
  • Reflect.preventExtensions(target):类似于 Object.preventExtensions,禁止对象的扩展,返回一个Boolean

    • target:禁止扩展的对象
  • Reflect.getOwnPropertyDescriptor(target, propertyKey):类似于 Object.getOwnPropertyDescriptor,获取对象某个自有属性的描述符,如果对象中存在该属性,则返回对应的属性描述符, 否则返回 undefined

    • target:要获取属性描述符的对象
    • propertyKey:属性的键名
  • Reflect.defineProperty(target, propertyKey, attributes):类似于 Object.defineProperty,在对象上定义属性,如果设置成功就会返回 true

    • target:要在其上定义属性的对象
    • propertyKey:属性的键名
    • attributes:属性的描述符对象
  • Reflect.ownKeys(target):返回一个包含所有自身属性(不包含继承属性)的数组。(类似于 Object.keys(), 但不会受enumerable影响)

    • target:要获取键的对象
const obj = {
  name: "obj",
  age: 18,
};
console.log(Reflect.setPrototypeOf(obj, {})); // true
console.log(Reflect.getPrototypeOf(obj)); // {}
console.log(Reflect.isExtensible(obj)); // true
console.log(Reflect.preventExtensions(obj)); // true
console.log(Reflect.getOwnPropertyDescriptor(obj, "age")); // {value: 18, writable: true, enumerable: true, configurable: true}
console.log(Reflect.ownKeys(obj)); // ['name', 'age']
console.log(Reflect.defineProperty(obj, "height", { value: 188 })); // false,因为什么设了preventExtensions
console.log(obj); // {name: 'obj', age: 18}

理解 Proxy / Reflect 配合使用

首先必须先理解 Proxy 中的 receiverReflect 中的 receiver

Proxy 中的 receiver

receiver 存在的意义就是为了正确的传递上下文,receiver 不仅仅代表的是 Proxy 代理对象本身,同时也许他会代表继承 Proxy 的那个对象

简单理解

上面我们学习setget的时候,参数receiver我们将它理解成为代理对象本身

const obj = {
  name: "obj",
  age: 18,
  running() {
    console.log("running");
  },
};
const proxy = new Proxy(obj, {
  set(target, key, newValue, receicer) {
    target[key] = newValue;
    console.log(receicer == proxy); // true
  },
  get(target, key, receicer) {
    console.log(receicer == proxy); // true
    return target[key];
  },
})
proxy.name == 'proxy'
console.log(proxy.name);

深入理解

receiver 的确是可以表示代理对象,但其实receiver 是执行 getset 操作时的实际调用对象

  • get 捕获器中receiver 是访问属性时的实际调用者对象,如果是通过某个对象(例如子对象)访问代理对象的属性,这个对象就是 receiver

  • set 捕获器中receiver 是设置属性时的对象,如果是在一个继承关系链中通过子对象进行设置操作,receiver 就是子对象

  • 当使用 Object.setPrototypeOf 让一个对象继承另一个代理对象时,receiver 参数可以帮助区分是谁在调用(是原型对象还是继承者对象)

我们看下面例子来理解一下,例子中的receiver参数就是baz对象

const obj = {
  name: "obj",
  age: 18,
};
const proxy = new Proxy(obj, {
  set(target, key, newValue, receiver) {
    console.log("set", receiver); // 只是打印 receiver 对象的引用,不涉及属性读取,不会触发get进入循环
      console.log(receiver == proxy); // false
      console.log(receiver == baz); // true
      target[key] = newValue;
    },
    get(target, key, receiver) {
          // console.log("get", receiver); // 相当于在外面写console.log(proxy),因此会进入循环,因为receiver是baz继承自 proxy,可使用Reflect解决
    console.log(receiver == proxy); // false 对象引用的比较不会陷入循环
    console.log(receiver == baz); // true
    return target[key];
  },
});
const baz = {
  name: "baz",
  // __proto__: proxy
};
// baz 对象的原型变成了 proxy,即 baz 继承了 proxy
Object.setPrototypeOf(baz, proxy);
baz.age = 30; // 执行set
console.log(baz.age); // 30,执行get

Reflect 中的 receiver

单独使用时

receiver 是在对象访问或修改时,指向当前操作对象的上下文。在某些情况下,它可以控制或影响 this 的指向

const obj = {
name: "obj",
age: 18,
set height(value) {
  console.log("set", this); // {}  这里的this就是Reflect.set传入的receiver
},
get height() {
  console.log("get", this); // {name: 'obj', age: 18}  这里的this就是Reflect.get传入的receiver
  },
};
console.log(Reflect.set(obj, "height", "188", {}));
console.log(Reflect.get(obj, "height")); // 不传receiver,就会被设置为target,即obj

结合Proxy

receiver 的作用

  • 对象继承:当对象从另一个对象继承属性时,receiver 可以确保继承者(而非父类对象)是属性访问的上下文

  • 代理拦截:代理对象通过 Reflect 使用 receiver,可以将操作传递到目标对象,同时保留 this 的正确指向

看下面代码我们来一点点理解:

const obj = {
  name: "obj",
  age: 18,
  set height(value) {
    console.log("set", this);
  },
  get height() {
    console.log("get", this); // {name: 'obj', age: 18}
    return 170 + this.age;
  },
};
const proxy = new Proxy(obj, {
  set(target, key, newValue, receiver) {
    console.log(receiver); // baz
    Reflect.set(target, key, newValue);
  },
  get(target, key, receiver) {
    console.log(target === obj); // true
    return Reflect.get(target, key);
  },
});
const baz = {
  age: 28,
};
// baz 对象的原型变成了 proxy,即 baz 继承了 proxy
Object.setPrototypeOf(baz, proxy);
console.log(baz.height); // 188

上面代码分析:

  • 不使用 Reflect.set()Reflect.get()receiver 参数时,默认设 receivertarget(即obj)

  • objgetter 方法取到的 this.ageobj.age 值为 18,因此我们取到的 baz.height 的值是 188 ,但值是错的按理应该是 198

那么我们修改一下代码:只需要把 receiver 传过去改变成正确的 this

const obj = {
  name: "obj",
  age: 18,
  set height(value) {
    console.log("set", this);
  },
  get height() {
    console.log("get", this); // baz:{age: 28, height: 198}
    return 170 + this.age;
  },
};
const proxy = new Proxy(obj, {
  set(target, key, newValue, receiver) {
    console.log(receiver);
    Reflect.set(target, key, newValue, receiver);
  },
  get(target, key, receiver) {
    console.log(target === obj); // true
    console.log(receiver === baz); // true
    return Reflect.get(target, key, receiver);
  },
});
const baz = {
  age: 28,
};
// baz 对象的原型变成了 proxy,即 baz 继承了 proxy
Object.setPrototypeOf(baz, proxy);
console.log(baz.height); // 198

到这里相信都能很清楚的理解 Reflectreceiver 的作用了,简单总结就是它可以修改属性访问中的 this 指向,让其指向传入的 receiver 对象

实现监听

  • 实现代理对象的目的,不再直接操作原对象

  • Reflect有返回结果值,可以判断操作是否成功

  • Reflect.set/get 最后一个参数 receiver 可以决定对象访问器 setter/getterthis 指向

const obj = {
  _name: "obj",
  set name(value) {
    console.log("set", this);
    this._name = value;
  },
  get name() {
    console.log("get", this);
    return this._name;
  },
};
const proxy = new Proxy(obj, {
  set(target, key, newValue, receiver) {
    const isSuccess = Reflect.set(target, key, newValue, receiver);
    if (!isSuccess) throw new Error(`set ${key}值失败`);
  },
  get(target, key, receiver) {
    const success = Reflect.get(target, key, receiver);
    if (!success) throw new Error(`get ${key}值失败`);
    return success;
  },
});
proxy.name = "proxy";
console.log(proxy.name); // proxy

const baz = {
  _name: "baz",
};
// baz 对象的原型变成了 proxy,即 baz 继承了 proxy
Object.setPrototypeOf(baz, proxy);
console.log(baz.name); // baz
你好!关于"vue3proxy结合reflect"的问题,我理解你可能想了解如何在Vue 3中使用Proxy与Reflect结合起来。Vue 3是一个流行的JavaScript框架,而Proxy和Reflect则是ES6中新增的特性。 在Vue 3中,可以使用Proxy对象来代理Vue实例,以便捕获对数据的访问和修改。Proxy对象可以拦截并处理对Vue实例的操作,比如读取、设置和删除属性等。而Reflect对象提供了一组用于操作对象的方法,比如Reflect.get()、Reflect.set()和Reflect.deleteProperty()等。 结合使用Proxy和Reflect可以提供更灵活的控制和监控能力。你可以通过在Vue实例上创建一个代理对象,来拦截对数据的访问和修改,并在代理处理器中使用Reflect方法来操作实际的数据。这样可以轻松地实现对数据的拦截、校验、代理等功能。 下面是一个简单的示例代码,展示了如何在Vue 3中使用Proxy和Reflect结合起来: ```javascript const data = { message: 'Hello, Vue!', }; const proxy = new Proxy(data, { get(target, key) { console.log('Getting ' + key); return Reflect.get(target, key); }, set(target, key, value) { console.log('Setting ' + key + ' to ' + value); return Reflect.set(target, key, value); }, }); // 创建Vue应用 const app = Vue.createApp({ data() { return proxy; // 使用代理对象 }, }); app.mount('#app'); ``` 在上面的代码中,我们创建了一个名为data的普通对象,并使用Proxy对象创建了一个代理对象proxy。在代理对象的get和set处理器中,我们分别使用Reflect.get和Reflect.set来操作实际的数据。这样,当我们通过Vue实例访问或修改数据时,会触发代理处理器,并通过Reflect方法操作实际的数据。 希望这个例子能帮助到你,如果还有其他问题,请随时提问!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值