JS - 代理模式

129 阅读3分钟

代理可以让代码避免直接操作目标对象,从而保证目标对象的隐秘性,也可以跟踪属性的访问,拦截一些操作,保证操作是可控的,代理是 js 对象的透明抽象层,每一个拦截器都对应着一个同名的 Reflect API,Reflect API 是绝大部分 js 对象 API 的基础

跟踪属性访问/隐藏属性

在 Proxy 中第二个参数是拦截器配置,拦截器可以指导对象属性什么时候被访问/查询/操作/更新/删除/定义,把对某个目标对象的拦截器配置上,就可以监控这个对象如何被访问过

所有的 const proxy = new Proxy(target, interceptorHandler) ,可以读作为目标对象定义拦截器处理的代理(可见代码的可读性是多么让人见码知意)。

如下代码将会进行跟踪属性访问,隐藏属性值,数据绑定到某个记录器(可观察对象

const user = {
  name: "kj",
  age: 23,
  // 明文定义密码
  password: "31218",
};

// 记录属性访问的次数
const counter = {};

const proxy = new Proxy(user, {
  get(target, p, receiver) {
    counter[p] = Number.isInteger(counter[p]) ? ++counter[p] : 1;
    if (p === "password") {
      // 在获取密码的时候进行加密处理(伪加密啦)
      return target[p] + String(Math.random()).replace("0.", "");
    } else {
      return Reflect.get(target, p, receiver);
    }
  },
});

log(proxy.password); // 将会打印 31218xxx (xxx 为随机数值)
log(proxy.name); // log: kj
log(proxy.name); // log: kj

log(counter); // log: { password: 1, name: 2 }; name 访问了两次,password 访问了一次

属性验证

所有赋值操作都会触发 set 捕获器,可以根据所赋的值决定是否允许赋值还是拒绝赋值

const person = {
  name: "jm",
  // 只允许 整数赋值给 age
  age: 23,
};

const p = new Proxy(person, {
  set(target, key, newValue, receiver) {
    if (key === "age" && !Number.isInteger(newValue)) {
      // 如果赋值的属性为 age 且 赋的值不是整数,则抛出类型错误,中断程序
      throw new TypeError(
        `age value must be number, but you given a ${typeof newValue}`
      );
    }

    // 否则交给 Reflect 默认处理
    Reflect.set(target, key, newValue, receiver);
  },
});

p.name = "new_name";
log(p.name); // log: new_name

p.age = 29;
log(p.age); // log: 29

p.age = "Nil"; // 触发 TypeError: age value must be number, but you given a string, 中断程序

函数与构造函数验证

Proxy 除了代理 对象,还可以代理 函数、构造函数(类),和验证属性赋值一样,可以对代理的函数配置对应的拦截器进行校验

验证函数参数

// 获取中位数
const median = (...nums) => {
  return nums.sort()[Math.floor(nums.length / 2)];
};

const proxyFn = new Proxy(median, {
  // 调用拦截器,代理函数调用的时候触发 apply 拦截器
  apply(target, thisArg, argArr) {
    if (argArr.some((v) => !Number.isFinite(v))) {
      throw new TypeError(`eixst non-number argument`);
    }
    // 如果没有报错,则交给 Reflect.apply 默认处理即可
    return Reflect.apply(target, thisArg, argArr);
  },
});

log(proxyFn(1, 2, 4, 3, 5)); // log: 3
proxyFn({}, 1, 2, 3, 4); // TypeError: eixst non-number argument,中断程序

验证构造函数

class User {
  constructor(id) {
    this._id = id;
  }
}

const ProxyUser = new Proxy(User, {
  construct(target, argArray, newTarget) {
    // 如果没有传递 id 过来,则报错终止程序
    if ([undefined, null].includes(argArray[0])) {
      throw new Error("User cannot be instantiated without id");
    }

    // 否则交给 Reflect.construct 默认处理
    return Reflect.construct(target, argArray, newTarget);
  },
});

const p1 = new ProxyUser("id1");
const p2 = new ProxyUser(); // Error: User cannot be instantiated without id