Proxy代理,讲得很细

316 阅读14分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情

大家都已经知道了,Vue3就是通过Proxy实现数据响应式原理的,那么proxy为什么可以实现数据响应式?经常和Proxy一起出现的Reflect又是用来干嘛的?Proxy还有一些什么运用场景呢?

Proxy是什么?

Proxy是JS的一个原生对象,它可以用于创建一个对象的代理,从而实现基本操作拦截和自定义行为

  • 原生对象

    Proxy是ES6新增的,也就是2015年才出现的,Vue2是2016年发布使用的,所以你知道Vue2为什么不是使用Proxy而是使用Object.definePropety了吧;听到ES6不知道你会不会或多或少的想到兼容性问题,由于ES5的限制,Proxy无法被转译成ES5,所以在一些老旧的浏览器上(手动@某E),Proxy是无法使用的,所以Vue3在Proxy无法使用的时候,就会进行降级,还是采用Object.definePropety的方式

  • 创建一个对象的代理

    看到创建一个对象,就莫名的想到new(毕竟单身的你,时常被同事调侃自己new一个对象),细心的小伙伴早已经发现了,Proxy的首字母是大写的,还记得那个不成文的规定么,构造函数的首字母一般是大写的,使用new操作符进行调用,是的没错,Proxy就是一个构造器,也是用new去创建一个对象的代理,所以被创建的对象的原型就是Proxy对象啦

    Proxy是创建的是一个对象的代理,而Object.definePropety是在一个对象上定义新的属性或修改一个已存在的属性;所以是对一整个对象代理效率高,还是一个属性一个属性定义效率高?结果不言而喻。

    任何类型的对象都能被代理,包括内置的数组,函数,甚至另一个Proxy对象;所以你知道Vue2为什么监听不到数组的变化,而Vue3可以了吧

  • 基本操作

    Proxy可以拦截以下13种操作

    apply-拦截函数调用

    construct-拦截new操作符

    defineProperty-拦截对象的Object.definePropety操作

    delete-拦截对象属性的删除操作

    get-拦截对象的读取属性操作

    getOwnPropertyDescriotor-拦截对象Object.getOwnPropertyDescriptor操作

    getPrototypeOf-拦截读取代理对象的原型操作

    has-拦截in操作符

    isExtensible-拦截对象的Object.isExtensible操作

    ownKeys-拦截Object.getOwnPropertyNamesObject.getOwnPropertySymbols操作

    preventExtensions-拦截Object-preventExtensions操作

    set-拦截设置属性值操作

    setPrototypeOf-拦截Object.setPrototypeOf操作

  • 自定义行为

    通过拦截上面的13种操作,可以自定义操作,这样你创建的代理对象进行上面的13种操作,都会触发你自定义的行为;你想让它往东它就往东,你没让它干嘛他就按照默认的行为干嘛。

Proxy使用

new Proxy(target,handler)
  • 参数

    target-它可以是任何类型的对象,包括内置数组,函数甚至是另一个Proxy对象

    handler-它也是一个对象,他的属性提供了某些操作(上面的13中操作)发生是所对应的处理函数

this的指向

const person = {
  name: 'George',
  getThis: function () {
    console.log(this);
  }
}
​
const personProxy = new Proxy(person, {
  get: function (target, property, receiver) {
    console.log(this); // 打印的是handler对象,handler中的this指向handler
    return target[property]
  }
})
​
person.getThis()  // 谁最后调用,this就指向谁,打印的是person对象
personProxy.getThis() // 打印的personProxy对象

当然如果你用匿名函数,那他们的this都指向window了

const person = {
  name: 'George',
  getThis: () => {
    console.log(this);
  }
}
​
const personProxy = new Proxy(person, {
  get: (target, property, receiver) => { // 访问回调    
    console.log(this); // 打印的是Window对象
    console.log(receiver); // 打印的是personProxy对象   
    return target[property]
  } 
})
​
person.getThis() // 打印的是Window对象
personProxy.getThis() // 打印的是Window对象

常见的几种拦截属性

handler.get

get可以创造一些本来没有的属性的返回值,可以在取值的时候对数据进行加工操作等,get 方法可以返回任何值。

let Moutai = {
  price: 888,
  degrees: 52
}
​
const MaotaiDealer = new Proxy(Moutai, {
  get: (target, property, receiver) => { // 访问回调    
    if (property === 'price') {
      return 1599
    }
    if (property === 'address') {
      return '贵州省仁怀市茅台镇'
    }
    return target[property]
  },
  set: () => { }
})
console.log(Moutai.price); // 888
console.log(Moutai.degrees);  // 52
console.log(Moutai.address);  // undefinedconsole.log(MaotaiDealer.price);  // 1599
console.log(MaotaiDealer.degrees);  // 52
console.log(MaotaiDealer.address); // 贵州省仁怀市茅台镇
  • target--目标对象
  • property--被获取的属性名
  • receiver--Proxy实例

如果把对象的某个属性配置修改成不可配置,不可写,又自定了handler.get的函数返回不同的值,那么去获取该属性时就会报错,如果要访问的目标舒心是不可写以及不可配置的,则返回的值必须与该目标属性的值相同

let Moutai = {
  price: 888,
  degrees: 52
}
​
Object.defineProperty(Moutai, 'price', {
  configurable: false,
  writable: false
})
​
const MaotaiDealer = new Proxy(Moutai, {
  get: (target, property, receiver) => { // 访问回调    
    if (property === 'price') {
      return 1599
    }
    return target[property]    
  }
​
})
console.log(MaotaiDealer.price);  //TypeError: 'get' on proxy: property 'price' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value (expected '888' but got '1599')
console.log(Moutai.price); // 888
console.log(MaotaiDealer.degrees); // 52
handler.set

set操作一般用于对于要赋值的数进行过滤,加工或是权限设置,set() 方法应当返回一个布尔值。

  • 返回 true 代表属性设置成功。
  • 在严格模式下,如果 set() 方法返回 false,那么会抛出一个 TypeError 异常。
let Moutai = {
  price: 888,
  degrees: 52
}
const MoutaiManufacturer = new Proxy(Moutai, {
  get: (target, property, receiver) => {
    if (property === 'address') {
      return '贵州省仁怀市茅台镇'
    }
    return target[property]
  },
  set: (target, property, value, receiver) => {
    if (property === 'address') {
      return false
    }
    target[property] = value
    return true
  }
})
MoutaiManufacturer.price = 1299
console.log(MoutaiManufacturer.price); // 1299
console.log(Moutai.price);// 1299MoutaiManufacturer.address = '广东省深圳市'
console.log(MoutaiManufacturer.address); // 贵州省仁怀市茅台镇
  • target--目标对象
  • property--被设置的属性名
  • value--新属性的值
  • receiver--Proxy实例

handler.set() 方法用于拦截设置属性值的操作,如果违背以下的约束条件,proxy 会抛出一个 TypeError 异常:

  • 若目标属性是一个不可写及不可配置的数据属性,则不能改变它的值。
  • 如果目标属性没有配置存储方法,即 [[Set]] 属性的是 undefined,则不能设置它的值。
  • 在严格模式下,如果 set() 方法返回 false,那么也会抛出一个 TypeError 异常。
handler.deleteProperty

handler.deleteProperty() 方法用于拦截删除属性的操作,deleteProperty 必须返回一个 Boolean 类型的值,表示了该属性是否被成功删除。如果目标对象的属性是不可配置的,那么该属性不能被删除。

let Moutai = {
  price: 888,
  degrees: 52
}
​
​
let MaotaiDealer = new Proxy(Moutai, {
  deleteProperty: (target, property) => {
    if (property === 'price') {
      return false
    }
    delete target[property]
    return true
  }
})
​
delete MaotaiDealer.degrees // true
  • target--目标对象
  • property--被设置的属性名
handler.apply

handler.apply() 方法用于拦截函数的调用,apply 方法可以返回任何值。target 必须是可被调用的。也就是说,它必须是一个函数对象。

function sum(a, b) {
  return a + b
}
​
const proxyFu = new Proxy(sum, {
  apply: (target, thisArg, argumentsList) => {
    return target(...argumentsList) * 3
  }
})
sum(3, 5) // 8
proxyFu(3, 5) // 24
  • target--目标对象
  • thisArg--被调用时的上下文对象
  • argumentsList--被调用时的参数数组

Reflect是什么?

Reflect 是一个内置的对象,字面意思是“反射”,它提供拦截 JavaScript 操作的方法。这些拦截方法和Proxy中的13中拦截方法命名相同

  • 内置的对象

    Reflect与大多数全局对象不同,Reflect不是一个函数对象,并非一个构造函数,所以他是不可构造的,即不能通过new操作符调用,或者将Reflect对象作为函数来调用。Reflect的所有属性和方法都是静态的(就像Math对象)

  • 操作方法

    Reflect 对象提供了以下静态方法,这些方法与Proxy拦截方法的命名相同,其中有些方法和Object相同

    • Reflect.apply(target,thisArgument,argumentsList)
    • Reflect.construct(target,argumentsList[, newTarget])
    • Reflect.defineProperty(target, propertyKey, attributes)
    • Reflect.deleteProperty(target, propertyKey)
    • Reflect.get(target, propertyKey[, receiver])
    • Reflect.getOwnPropertyDescriptor(target, propertyKey)
    • Reflect.getPrototypeOf(target)
    • Reflect.has(target, propertyKey)
    • Reflect.isExtensible(target)
    • Reflect.ownKeys(target)
    • Reflect.preventExtensions(target)
    • Reflect.set(target, propertyKey, value[, receiver])
    • Reflect.setPrototypeOf(target, prototype)
  • 命名相同

    看完上面的命名,发现确实一样,那他们之间对应着什么样联系呢?其实Reflect的方法是用来操作对象的,可以看做是Object的升级版,增加了兼用性,对浏览器和用户友好;这是因为在早期的ECMA规范中没有考虑到这种对对象本身的操作如何设计会更规范,所以将这些API放到了Object上面,但是Object作为一个构造函数,这些操作实际上放到它身上并不合适;另外还包含一些类似于in、delete操作符,让JS看起来会有一些奇怪,所以在ES6增加了Relect,把这些操作都集中到Relect对象上。简单理解就是Object这个函数上的属性太杂了,大概有20种左右,虽然其中包含了对象接口。但是这不太好,我们需要一个专门的对象来做这个事情。显然不可能重新设计Object,毕竟兼容性才是大哥。于是Reflect应运而生,取名为Reflect,是因为它像镜子一样无差别的将操作反射给对象的内部接口。

    • 修改某些 Object 方法的返回结果,让其变得更合理

      Object.defineProperty(obj, name, desc)
      // 在无法定义属性时,会抛出一个错误
       
      Reflect.defineProperty(obj, name, desc)
      // 在无法定义属性时,则会返回  false。
      
    • Object 操作都变成函数行为。某些 Object 操作是命令式,让它们变成了函数行为

      name in obj
      delete obj[name]
        
      Reflect.has(obj, name)
      Reflect.deleteProperty(obj, name)
      
    • Reflect 对象的方法与 Proxy 对象的方法一一对应,只要是 Proxy 对象的方法,就能在 Reflect 对象上找到对应的方法。

Relect使用

Reflect.apply(target,thisArgument,argumentsList)

通过指定的参数列表发起对目标 (target) 函数的调用;该方法和ES5中的Function.prototype.apply()方法类似:调用一个方法并显示地指定this变量和参数列表(arguments),参数列表可以是数组或类数组的对象

Reflect.apply("".charAt, "ponies", [3])  // i
// 相当于 'ponies'.charAt(3)
Reflect.apply(Math.floor, undefined, [1.75]); // 1
// 相当于 Math.floor(1.75)
  • target--目标函数
  • thisArgument--target函数调用时绑定的this对象
  • argumentsList--target函数调用时传入的实参列表,该参数是一个数组或类数组的对象

Reflect.construct(target,argumentsList[, newTarget])

该方法有点像new操作符构造函数,相当于new target(...args)

function Person(name) {
  this.name=name
}
const zhansanObj= Reflect.construct(Person,['张三'])
// 相当于 const zhansanObj = new Person('张三')
  • target--被运行的目标构造函数(必须要是构造函数,如果不是会抛出异常)

  • argumentsList--类数组,目标构造函数调用时的参数

  • newTarget--可选,作为新创建对象的原型对象的constructor属性(必须要是构造函数,如果不是会抛出异常)

    function Person(name) {
      this.name=name
    }
    ​
    function NewTarget(name){
      this.name=name
    }
    const zhansanObj= Reflect.construct(Person,['张三'],NewTarget)
    // 相当于 const zhansanObj = new NewTarget('张三')
    

Reflect.defineProperty(target, propertyKey, attributes)

基本等同于 Object.defineProperty() 方法,唯一不同是Object.defineProperty() 返回的是 Boolean 值;Object.defineProperty方法,如果成功则返回一个对象,否则抛出一个 TypeError 。如果target不是 Object,抛出一个TypeError

let obj = {}
Reflect.defineProperty(obj,'x',{value:10})
Object.defineProperty(obj,'y',{value:20})
console.log(obj); // {x:10,y:20}
  • target--目标对象
  • propertyKey--要定义或修改的属性的名称
  • attributes--要定义或修改的属性的描述。

Reflect.deleteProperty(target, propertyKey)

用于删除属性,它很像delete obj.prop,返回值为Boolean 表明该属性是否被成功删除。如果target不是 Object,抛出一个 TypeError

let Moutai = {
  price: 888,
  degrees: 52
}
Reflect.deleteProperty(Moutai,price) // true
// 相当于
delete Moutai.price // true
  • target--删除属性的目标对象。
  • propertyKey--需要删除的属性的名称。

Reflect.get(target, propertyKey[, receiver])

Reflect.get方法允许你从一个对象中取属性值。就如同属性访问器语法,但却是通过函数调用来实现。如果target不是 Object,抛出一个 TypeError

let Moutai = {
  price: 888,
  degrees: 52
}
Reflect.get(Moutai,'price') // 888
Reflect.get(["zero", "one"], 1); // "one"
  • target--需要取值的目标对象
  • propertyKey--需要获取的值的键值
  • receiver--如果target对象中制定了getterreceiver则为getter调用时的this

Reflect.getOwnPropertyDescriptor(target, propertyKey)

静态方法 Reflect.getOwnPropertyDescriptor()Object.getOwnPropertyDescriptor() 方法相似。如果在对象中存在,则返回给定的属性的属性描述符。否则返回 undefined。唯一不同在于如何处理非对象目标,如果该方法的第一个参数不是一个对象,Reflect.getOwnPropertyDescriptor()会报错,Object.getOwnPropertyDescriptor() 将参数强制转换为一个对象处理。

Reflect.getOwnPropertyDescriptor({x: "hello"}, "x");
// {value: "hello", writable: true, enumerable: true, configurable: true}
// 相当于  Object.getOwnPropertyDescriptor({x: "hello"}, "x");
​
Reflect.getOwnPropertyDescriptor("foo", 0);  // TypeError: "foo" is not non-null object
Object.getOwnPropertyDescriptor("foo", 0);// { value: "f", writable: false, enumerable: true, configurable: false }
  • target--需要取值的目标对象
  • propertyKey--获取自己的属性描述符的属性的名称

Reflect.getPrototypeOf(target)

Reflect.getPrototypeOf()Object.getPrototypeOf()方法几乎是一样的。都是返回指定对象的原型(即内部的 [[Prototype]] 属性的值);如果target不是 Object,抛出一个 TypeErrorReflect 抛异常,Object 强制类型转换;

Reflect.getPrototypeOf({}); // Object.prototype
Reflect.getPrototypeOf(Object.prototype); // null
Reflect.getPrototypeOf(Object.create(null)); // null
  • target--获取原型的目标对象

Reflect.has(target, propertyKey)

Reflect.has()作用与 in 操作符相同,返回值Boolean, 如果target不是 Object,抛出一个 TypeError

Reflect.has({x: 0}, "x"); // true
Reflect.has({x: 0}, "y"); // false// 如果该属性存在于原型链中,返回 true
Reflect.has({x: 0}, "toString");
  • target--目标对象
  • propertyKey--属性名,需要检查目标对象是否存在此属性。

Reflect.isExtensible(target)

Reflect.isExtensible() 判断一个对象是否可扩展(即是否能够添加新的属性),返回值Boolean, ,它 Object.isExtensible() 方法相似,但有一些不同,如果该方法的第一个参数不是一个对象,Reflect.isExtensible()会报错,Object.isExtensible() 将参数强制转换为一个对象处理。

let empty = {};
Reflect.isExtensible(empty); // === trueReflect.preventExtensions(empty);
Reflect.isExtensible(empty); // === false
​
​
Reflect.isExtensible(1);
// TypeError: 1 is not an objectObject.isExtensible(1);
// false
  • target--检查是否可扩展的目标对象。

Reflect.ownKeys(target)

Reflect.ownKeys() 返回一个由目标对象自身的属性键组成的数组。它的返回值等同于 Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))。如果target不是 Object,抛出一个 TypeError

Reflect.ownKeys({z: 3, y: 2, x: 1}); // [ "z", "y", "x" ]
Reflect.ownKeys([]); // ["length"]
  • target--获取自身属性键的目标对象

Reflect.preventExtensions(target)

Reflect.preventExtensions()方法阻止新属性添加到对象 (例如:防止将来对对象的扩展被添加到对象中)。返回值Boolean, 该方法与 Object.preventExtensions()相似,但有一些不同点,如果该方法的第一个参数不是一个对象,Reflect.preventExtensions()会报错,Object.preventExtensions() 将参数强制转换为一个对象处理。

// Objects are extensible by default.
let empty = {};
Reflect.isExtensible(empty); // === true// ...but that can be changed.
Reflect.preventExtensions(empty);
Reflect.isExtensible(empty); // === false
  • target--阻止扩展的目标对象。

Reflect.set(target, propertyKey, value[, receiver])

Reflect.set 方法允许你在对象上设置属性。它的作用是给属性赋值并且就像obj.prop = value 语法一样,但是它是以函数的方式。返回一个 Boolean 值表明是否成功设置属性。如果target不是 Object,抛出一个 TypeError

// Object
let obj = {};
Reflect.set(obj, "prop", "value"); // true
obj.prop; // "value"// Array
let arr = ["duck", "duck", "duck"];
Reflect.set(arr, 2, "goose"); // true
arr[2]; // "goose"// 它可以截断数组。
Reflect.set(arr, "length", 1); // true
arr; // ["duck"];// 只有一个参数,propertyKey和value“未定义”。
var obj = {};
Reflect.set(obj); // true
Reflect.getOwnPropertyDescriptor(obj, "undefined");
// { value: undefined, writable: true, enumerable: true, configurable: true }
  • target--设置属性的目标对象。
  • propertyKey--设置的属性的名称。
  • value--设置的值。
  • receiver--如果遇到 setterreceiver则为setter调用时的this值。

Reflect.setPrototypeOf(target, prototype)

Reflect.setPrototypeOf()Object.setPrototypeOf()方法是一样的。它可设置对象的原型(即内部的 [[Prototype]] 属性)为另一个对象或 null,如果操作成功返回 true,否则返回 false。如果target不是 Object,抛出一个 TypeError

Reflect.setPrototypeOf({}, Object.prototype); // true// 它可以将对象的[[Prototype]]更改为null
Reflect.setPrototypeOf({}, null); // true// 如果目标不可扩展,则返回false。
Reflect.setPrototypeOf(Object.freeze({}), null); // false// 如果导致原型链循环,则返回false。
let target = {};
let proto = Object.create(target);
Reflect.setPrototypeOf(target, proto); // false
  • target--设置原型的目标对象
  • propertyKey--对象的新原型(一个对象或 null)。

了解了Relect怎么使用,我们就可以把之前的写法改一下,比如target[property]改成Reflect.get(target,property);比如target[property] = value改成Reflect.set(target,property,value);Relect一般是和Proxy同时使用的

Proxy使用例子

统计函数被调用

const countExecute = (fn) => {
  let count = 0
  return new Proxy(fn, {
    apply(target, ctx, args) {
      ++count
      console.log('ctx上下文:', ctx);
      console.log(`第${count} 次 调用${fn.name} `);
      return Reflect.apply(target, ctx, args)
    }
  })
}
​
const getSum = (...args) => {
  return args.reduce((pre, cur) => pre + cur, 0)
}
​
const useSum = countExecute(getSum)
​
useSum(1, 2, 3) // ctx上下文: undefined , 第1 次 调用getSum 
useSum.apply(window, [4, 5, 6]) //ctx上下文:Window , 第2 次 调用getSum 
let obj = {}
useSum.apply(obj, [7,8,9]) //ctx上下文: {}, 第3 次 调用getSum 

实现一个节流功能

const throttleByProxy = (fn, time) => {
  let lastTime = 0
  return new Proxy(fn, {
    apply(target, ctx, args) {
      const nowTime = Date.now()
      if (nowTime - lastTime > time) {
        lastTime = nowTime
        Reflect.apply(target, ctx, args)
      }
    }
  })
​
}
​
const longTimeStamp = () => console.log(Date.now());
​
window.addEventListener('scroll', throttleByProxy(longTimeStamp, 300));

实现观察者模式

const list = new Set()
const observe = (fn) => list.add(fn)
const observable = (obj) => {
  return new Proxy(obj, {
    set(target, key, value, receiver) {
      const result = Reflect.set(target, key, value, receiver)
      list.forEach((observe) => observe())
      return result
    }
  })
}
​
const person = observable({name:'George',age:20})  // 使用Proxy创建代理对象
const App = () =>{
  console.log(`App -> name: ${person.name}, age: ${person.age}`);
}
observe(App); // 加入观察者
person.name='XXX'  //  person属性变化,执行观察者 App -> name: XXX, age: 20

最后

伴随着Reflect,Proxy降世,为js带来了元编程!下次一定!

如果你看到了这里,烦请大佬点个赞,鼓励一下小弟学习。