什么是proxy?

230 阅读4分钟

在 Proxy 之前,JavaScript 中就提供过 Object.defineProperty,允许对对象的 getter/setter 进行拦截,在vue3之前,vue的双向绑定是通过defineProperty实现的,在vue3之后重构为proxy,那么,什么是proxy呢。

proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,属于一种“元编程”,即对编程语言进行编程。

  • 元编程(英语:Metaprogramming,又译超编程,是指某类计算机程序的编写,这类计算机程序编写或者操纵其它程序(或者自身)作为它们的数据,或者在运行时完成部分本应在编译时完成的工作。这意味着可以编写出这样的程序:它能够读取、生成、分析或者转换其它程序,甚至在运行时修改程序自身(反射)。

概述

proxy译为代理,可以理解成在目标对象之前架设一层拦截,将所有本该我们手动编写的程序交由代理来处理。

语法

  • target 要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理
  • handler 一个通常以函数作为属性的对象,用来定制拦截行为
const proxy = new Proxy(target, handle)

举个栗子:

const origin = {}
const obj = new Proxy(origin, {
  get: function (target, propKey, receiver) {
      return '10'
  }
});

obj.a // 10
obj.b // 10
origin.a // undefined
origin.b // undefined

这段代码中我们给一个空对象的get架设了一层代理,所有get操作都会直接返回我们定制的数字10,需要注意的是,代理只会对proxy对象生效,如上方的origin就没有任何效果

常用方法
标题描述
get()属性读取操作的捕捉器
set()属性设置操作的捕捉器
has()in 操作符的捕捉器
deleteProperty()delete 操作符的捕捉器
ownKeys()Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器
apply()函数调用操作的捕捉器
construct()new 操作符的捕捉器

还有一些其他方法,不再举例。

基础示例,我们举例讲下get和set方法

get():
const handler = {
    get: function(obj, prop) {
        return prop in obj ? obj[prop] : 37;
    }
};

const p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;

console.log(p.a, p.b);      // 1, undefined
console.log('c' in p, p.c); // false, 37
set():
let validator = {
  set: function(obj, prop, value) {
    if (prop === 'age') {
      if (!Number.isInteger(value)) {
        throw new TypeError('The age is not an integer');
      }
      if (value > 200) {
        throw new RangeError('The age seems invalid');
      }
    }

    // The default behavior to store the value
    obj[prop] = value;
    // 表示成功
    return true;
  }
};

let person = new Proxy({}, validator);
person.age = 100;

console.log(person.age);  // 100
person.age = 'young';
// 抛出异常: Uncaught TypeError: The age is not an integer
person.age = 300;
// 抛出异常: Uncaught RangeError: The age seems invalid

可撤销的Proxy

proxy有一个唯一的静态方法,Proxy.revocable(target, handler)

Proxy.revocable()方法可以用来创建一个可撤销的代理对象

该方法的返回值是一个对象,其结构为: {"proxy": proxy, "revoke": revoke}

  • proxy 表示新生成的代理对象本身,和用一般方式 new Proxy(target, handler) 创建的代理对象没什么不同,只是它可以被撤销掉。
  • revoke 撤销方法,调用的时候不需要加任何参数,就可以撤销掉和它一起生成的那个代理对象。

该方法常用于完全封闭对目标对象的访问, 如下示例:

const target = { name: 'qingshan'}
const {proxy, revoke} = Proxy.revocable(target, handler)
proxy.name // 正常取值输出 qingshan
revoke() // 取值完成对proxy进行封闭,撤消代理
proxy.name // TypeError: Revoked

实用场景

有时我们想要一个number类型的数据,但是拿回来的却是string类型,我们可以proxy实现一个逻辑分离的数据格式验证器

const target = {
  _id: '1024',
  name:  'vuejs'
}

const validators = {  
    name(val) {
        return typeof val === 'string';
    },
    _id(val) {
        return typeof val === 'number' && val > 1024;
    }
}

const createValidator = (target, validator) => {
  return new Proxy(target, {
    _validator: validator,
    set(target, propkey, value, proxy){
      let validator = this._validator[propkey](value)
      if(validator){
        return Reflect.set(target, propkey, value, proxy)
      }else {
        throw Error(`Cannot set ${propkey} to ${value}. Invalid type.`)
      }
    }
  })
}

const proxy = createValidator(target, validators)

proxy.name = 'vue-js.com' // vue-js.com
proxy.name = 10086 // Uncaught Error: Cannot set name to 10086. Invalid type.
proxy._id = 1025 // 1025
proxy._id = 22  // Uncaught Error: Cannot set _id to 22. Invalid type 

this问题

proxy会改变target中的this指向,一旦proxy代理了argettarget内部的this则指向了proxy,而不是target

const target = {
  m: function () {
    console.log(this === proxy);
  }
};
const handler = {};

const proxy = new Proxy(target, handler);

target.m() // false
proxy.m()  // true

上面代码中,一旦proxy代理targettarget.m()内部的this就是指向proxy,而不是target。所以,虽然proxy没有做任何拦截,target.m()proxy.m()返回不一样的结果。

有些原生对象的内部属性,只有通过正确的this才能拿到,所以 Proxy 也无法代理这些原生对象的属性。

const target = new Date();
const handler = {};
const proxy = new Proxy(target, handler);

proxy.getDate();
// TypeError: this is not a Date object.

另外,Proxy 拦截函数内部的this,指向的是handler对象。

const handler = {
  get: function (target, key, receiver) {
    console.log(this === handler);
    return 'Hello, ' + key;
  },
  set: function (target, key, value) {
    console.log(this === handler);
    target[key] = value;
    return true;
  }
};

const proxy = new Proxy({}, handler);

proxy.foo
// true
// Hello, foo

proxy.foo = 1
// true