都快2023年了!赶快来熟悉一下ES6的Proxy用法吧

25,680 阅读2分钟

一、Proxy介绍

Proxy :用于创建一个对象的代理,从而实现基本操作的拦截和自定义

二、Proxy使用

Proxy为 构造函数,用来生成 Proxy实例

var proxy = new Proxy(target, handler)

target表示所要拦截的目标对象(任何类型的对象,包括原生数组,函数,甚至另一个代理)

handler通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时的代理行为

参数 handler 属性

关于handler拦截属性,有如下:

  • get(target,propKey,receiver):拦截对象属性的读取
  • set(target,propKey,value,receiver):拦截对象属性的设置
  • has(target,propKey):拦截propKey in proxy的操作,返回一个布尔值
  • deleteProperty(target,propKey):拦截delete proxy[propKey]的操作,返回一个布尔值
  • ownKeys(target):拦截Object.keys(proxy)for...in等循环,返回一个数组
  • getOwnPropertyDescriptor(target, propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象
  • defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc),返回一个布尔值
  • preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值
  • getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象
  • isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值
  • setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值
  • apply(target, object, args):拦截 Proxy 实例作为函数调用的操作
  • construct(target, args):拦截 Proxy 实例作为构造函数调用的操作

优先重点关注 get set deleteProperty 这三个操作

Reflect介绍

ES6中操作对象而提供的新 API,若需要在Proxy内部调用对象的默认行为,建议使用Reflect

基本特点:

  • 只要Proxy对象具有的代理方法,Reflect对象全部具有,以静态方法的形式存在
  • 修改某些Object方法的返回结果,让其变得更合理(定义不存在属性行为的时候不报错而是返回false
  • Object操作都变成函数行为

get()

get(target, propKey, receiver)接受三个参数,依次为目标对象、属性名和 proxy 实例本身

let person = {
  name: "Guest"
};
let proxy = new Proxy(person, {
  get: function(target, propKey, receiver) {
    return Reflect.get(target, propKey, receiver)
    // or
    // return target[propKey]
  }
});

proxy.name // "Guest"

get能够对数组增删改查进行拦截,下面是试下你数组读取负数的索引

function createArray(...elements) {
  let handler = {
    get(target, propKey, receiver) {
      let index = Number(propKey);
      if (index < 0) {
        propKey = String(target.length + index);
      }
      return Reflect.get(target, propKey, receiver);
    }
  };

  let target = [];
  target.push(...elements);
  return new Proxy(target, handler);
}

let arr = createArray('a', 'b', 'c');
arr[-1] // c

set()

set(target, propKey, value, receiver)方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身

现定义一个对象 规定 年龄输入整数时才被赋值,访问无效属性时控制台提醒 code ads

const obj = { name: "张三", age: 18 };
const proxy = new Proxy(obj, {
    get(target, prop) {
      if (prop in target) {
        return Reflect.get(target, prop);
      } else {
        console.error("字段不存在")
        return undefined;
      }
    },
    set(target, propKey, value, receiver) {
      if (propKey === "age") {
        if (typeof value === "number") {
          return Reflect.set(target, propKey, value, receiver);
          // or
          // target[propKey] = value 
          // return true
        } else {
          console.error("年龄只能输入正整数");
          return false;
        }
      } else {
        return false;
      }
    }
});
proxy.age = 20;  
console.log(proxy.age);  // 20
proxy.age = "22";
console.log(proxy.age);  // 20
console.log(proxy.test); // undefined

image.png

提醒:严格模式下,set代理如果没有返回true,就会报错

deleteProperty()

deleteProperty方法用于拦截delete操作,如果这个方法抛出错误或者返回false,当前属性就无法被delete命令删除

var handler = {
  deleteProperty (target, key) {
    invariant(key, 'delete');
    Reflect.deleteProperty(target,key)
    return true;
  }
};
function invariant (key, action) {
  if (key[0] === '_') {
    throw new Error(`无法删除私有属性`);
  }
}

var target = { _prop: 'foo' };
var proxy = new Proxy(target, handler);
delete proxy._prop
// Error: 无法删除私有属性

取消代理

Proxy.revocable(target, handler);

三、Reflect使操作变得更加合理

一招制敌 细读下面例子

let user = {
  _name: "Guest",
  get name() {
    return this._name;
  }
};

let userProxy = new Proxy(user, {
  get(target, prop, receiver) {
  return target[prop]
  }
});

let admin = {
  __proto__: userProxy,
  _name: "Admin"
};
console.log(admin.name); // ???

把admin的原型指向userProxy,再访问admin.name属性

admin自身不具备name属性,顺着原型链往上找name,user对name的访问做了返回值,返回当前对象的_name属性,也就是admin对象上面的_name 。这样看 admin.name 值就是 admin._name

有个关键点就在于userProxy 代理的是user对象,所以 userProxy 中 get的第一个参数 target 的值实际上是user,细品一下,这里的this 依旧是指向的是user,而不是admin。

若需要在Proxy内部调用对象的默认行为,建议使用Reflect

四、使用场景

Proxy其功能非常类似于设计模式中的代理模式,常用功能如下:

  • 拦截和监视外部对对象的访问
  • 降低函数或类的复杂度
  • 在复杂操作前对操作进行校验或对所需资源进行管理

使用 Proxy 保障数据类型的准确性

let data = { num: 0 };
data = new Proxy(data, {
    set(target, key, value, proxy) {
        if (typeof value !== 'number') {
            throw Error("属性只能是number类型");
        }
        return Reflect.set(target, key, value, proxy);
    }
});

data.num = "foo"
// Error: 属性只能是number类型
data.num = 1
// 赋值成功

声明一个私有的 apiKey,便于 api 这个对象内部的方法调用,但不希望从外部也能够访问 api._apiKey

let api = {
    _apiKey: 'kafakakafaka',
};
const RESTRICTED = ['_apiKey'];
api = new Proxy(api, {
    get(target, key, receiver) {
        if(RESTRICTED.indexOf(key) > -1) {
            throw Error(`${key} 不可访问.`);
        } 
        return Reflect.get(target, key, receiver);
    },
    set(target, key, value, receiver) {
        if(RESTRICTED.indexOf(key) > -1) {
            throw Error(`${key} 不可修改`);
        }
        return Reflect.set(target, key, value, receiver);
    }
});

console.log(api._apiKey)
api._apiKey = '987654321'
// 上述都抛出错误

还能通过使用Proxy实现观察者模式

观察者模式(Observer mode)指的是函数自动观察数据对象,一旦对象有变化,函数就会自动执行

observable函数返回一个原始对象的 Proxy 代理,拦截赋值操作,触发充当观察者的各个函数

const queuedObservers = new Set();

const observe = fn => queuedObservers.add(fn);
const observable = obj => new Proxy(obj, {set});

function set(target, key, value, receiver) {
  const result = Reflect.set(target, key, value, receiver);
  queuedObservers.forEach(observer => observer());
  return result;
}

观察者函数都放进Set集合,当修改obj的值,在会set函数中拦截,自动执行Set所有的观察者