Skip to content

JS 基础:proxy

proxy 在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。文档查阅

Proxy 是一种代理模式,常用于三个方面:

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

正因为此,可以做一些中间件相关的事情。

抽离校验模块

js
let obj = {
  pickyMethodOne: function (obj, str, num) {
    /* ... */
  },
  pickyMethodTwo: function (num, obj) {
    /*... */
  },
};

const argTypes = {
  pickyMethodOne: ['object', 'string', 'number'],
  pickyMethodTwo: ['number', 'object'],
};

obj = new Proxy(obj, {
  get: function (target, key, proxy) {
    var value = target[key];
    return function (...args) {
      var checkArgs = argChecker(key, args, argTypes[key]);
      return Reflect.apply(value, target, args);
    };
  },
});

function argChecker(name, args, checkers) {
  for (var idx = 0; idx < args.length; idx++) {
    var arg = args[idx];
    var type = checkers[idx];
    if (!arg || typeof arg !== type) {
      console.warn(`You are incorrectly implementing the signature of ${name}. Check param ${idx + 1}`);
    }
  }
}

obj.pickyMethodOne();
obj.pickyMethodTwo('wopdopadoo', {});
obj.pickyMethodOne({}, 'a little string', 123);
obj.pickyMethodOne(123, {});

私有属性

js
let api = {
  _apiKey: '123abc456def',
  getUsers: function () {},
  getUser: function (userId) {},
  setUser: function (userId, config) {},
};
const RESTRICTED = ['_apiKey'];
api = new Proxy(api, {
  get(target, key, proxy) {
    if (RESTRICTED.indexOf(key) > -1) {
      throw Error(`${key} is restricted. Please see api documentation for further info.`);
    }
    return Reflect.get(target, key, proxy);
  },
  set(target, key, value, proxy) {
    if (RESTRICTED.indexOf(key) > -1) {
      throw Error(`${key} is restricted. Please see api documentation for further info.`);
    }
    return Reflect.get(target, key, value, proxy);
  },
});
// 以下操作都会抛出错误
console.log(api._apiKey);
api._apiKey = '987654321';

访问日志

js
let api = {
  _apiKey: '123abc456def',
  getUsers: function () {
    /* ... */
  },
  getUser: function (userId) {
    /* ... */
  },
  setUser: function (userId, config) {
    /* ... */
  },
};
function logMethodAsync(timestamp, method) {
  setTimeout(function () {
    console.log(`${timestamp} - Logging ${method} request asynchronously.`);
  }, 0);
}
api = new Proxy(api, {
  get: function (target, key, proxy) {
    var value = target[key];
    return function (...arguments) {
      logMethodAsync(new Date(), key);
      return Reflect.apply(value, target, arguments);
    };
  },
});
api.getUsers();

预警和拦截

js
let dataStore = {
  noDelete: 1235,
  oldMethod: function () {
    /*...*/
  },
  doNotChange: 'tried and true',
};
const NODELETE = ['noDelete'];
const NOCHANGE = ['doNotChange'];
const DEPRECATED = ['oldMethod'];
dataStore = new Proxy(dataStore, {
  set(target, key, value, proxy) {
    if (NOCHANGE.includes(key)) {
      throw Error(`Error! ${key} is immutable.`);
    }
    return Reflect.set(target, key, value, proxy);
  },
  deleteProperty(target, key) {
    if (NODELETE.includes(key)) {
      throw Error(`Error! ${key} cannot be deleted.`);
    }
    return Reflect.deleteProperty(target, key);
  },
  get(target, key, proxy) {
    if (DEPRECATED.includes(key)) {
      console.warn(`Warning! ${key} is deprecated.`);
    }
    var val = target[key];
    return typeof val === 'function'
      ? function (...args) {
          Reflect.apply(target[key], target, args);
        }
      : val;
  },
});
// these will throw errors or log warnings, respectively
dataStore.doNotChange = 'foo';
delete dataStore.noDelete;
dataStore.oldMethod();

过滤操作

js
let obj = {
  getGiantFile: function (fileId) {
    /*...*/
  },
};
obj = new Proxy(obj, {
  get(target, key, proxy) {
    return function (...args) {
      const id = args[0];
      let isEnroute = checkEnroute(id);
      let isDownloading = checkStatus(id);
      let cached = getCached(id);
      if (isEnroute || isDownloading) {
        return false;
      }
      if (cached) {
        return cached;
      }
      return Reflect.apply(target[key], target, args);
    };
  },
});

中断代理

js
let data = {
  username: 'korey',
};
const { proxy, revoke } = Proxy.revocable(data, {});
// logs 'devbryce'
console.log(proxy.username);
revoke();
// TypeError: Revoked
console.log(proxy.username);

解决对象属性为 undefined 的问题

js
(() => {
  let target = {};
  let handlers = {
    get: (target, property) => {
      target[property] = property in target ? target[property] : {};
      if (typeof target[property] === 'object') {
        return new Proxy(target[property], handlers);
      }
      return target[property];
    },
  };
  let proxy = new Proxy(target, handlers);
  console.log('z' in proxy.x.y); // false (其实这一步已经针对`target`创建了一个x.y的属性)
  proxy.x.y.z = 'hello';
  console.log('z' in proxy.x.y); // true
  console.log(target.x.y.z); // hello
})();

普通函数与构造函数的兼容处理

js
class Test {
  constructor(a, b) {
    console.log('constructor', a, b);
  }
}

// Test(1, 2) // throw an error
let proxyClass = new Proxy(Test, {
  apply(target, thisArg, argumentsList) {
    // 如果想要禁止使用非new的方式来调用函数,直接抛出异常即可
    // throw new Error(`Function ${target.name} cannot be invoked without 'new'`)
    return new (target.bind(thisArg, ...argumentsList))();
  },
});

proxyClass(1, 2); // constructor 1 2

包装 fetch

js
let handlers = {
  get(target, property) {
    if (!target.init) {
      // 初始化对象
      ['GET', 'POST'].forEach((method) => {
        target[method] = (url, params = {}) => {
          return fetch(url, {
            headers: {
              'content-type': 'application/json',
            },
            mode: 'cors',
            credentials: 'same-origin',
            method,
            ...params,
          }).then((response) => response.json());
        };
      });
    }

    return target[property];
  },
};
let API = new Proxy({}, handlers);

断言工具

js
let assert = new Proxy(
  {},
  {
    set(target, message, value) {
      if (!value) console.error(message);
    },
  },
);

assert["Isn't true"] = false; // Error: Isn't true
assert['Less than 18'] = 18 >= 19; // Error: Less than 18

统计函数调用次数

js
function orginFunction () {}
let proxyFunction = new Proxy(orginFunction, {
  apply (target, thisArg. argumentsList) {
    log(XXX)

    return target.apply(thisArg, argumentsList)
  }
})

实现双绑比 Object.defineProperty 的优势

  • 可以直接监听对象而非属性
  • 可以直接监听数组的变化
  • 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等