文章永久链接地址:http://pinggod.com/2016/%E5%AE%9E%E4%BE%8B%E8%A7%A3%E6%9E%90-ES6-Proxy-%E4%BD%BF%E7%94%A8%E5%9C%BA%E6%99%AF/

ES6 中的箭头函数、数组解构、rest 参数等特性一经实现就广为流传,但类似 Proxy 这样的特性却很少见到有开发者在使用,一方面在于浏览器的兼容性,另一方面也在于要想发挥这些特性的优势需要开发者深入地理解其使用场景。就我个人而言是非常喜欢 ES6 的 Proxy,因为它让我们以简洁易懂的方式控制了外部对对象的访问。在下文中,首先我会介绍 Proxy 的使用方式,然后列举具体实例解释 Proxy 的使用场景。

Proxy,见名知意,其功能非常类似于设计模式中的代理模式,该模式常用于三个方面:

  • 拦截和监视外部对对象的访问

  • 降低函数或类的复杂度

  • 在复杂操作前对操作进行校验或对所需资源进行管理

在支持 Proxy 的浏览器环境中,Proxy 是一个全局对象,可以直接使用。Proxy(target, handler) 是一个构造函数,target 是被代理的对象,handlder 是声明了各类代理操作的对象,最终返回一个代理对象。外界每次通过代理对象访问 target 对象的属性时,就会经过 handler 对象,从这个流程来看,代理对象很类似 middleware(中间件)。那么 Proxy 可以拦截什么操作呢?最常见的就是 get(读取)、set(修改)对象属性等操作,完整的可拦截操作列表请点击这里。此外,Proxy 对象还提供了一个 revoke 方法,可以随时注销所有的代理操作。在我们正式介绍 Proxy 之前,建议你对 Reflect 有一定的了解,它也是一个 ES6 新增的全局对象,详细信息请参考 MDN Reflect。

Basic

const target = {  name: 'Billy Bob',age: 15
};const handler = {  get(target, key, proxy) {const today = new Date();console.log(`GET request made for ${key} at ${today}`);return Reflect.get(target, key, proxy);}
};const proxy = new Proxy(target, handler);
proxy.name;
// => "GET request made for name at Thu Jul 21 2016 15:26:20 GMT+0800 (CST)"
// => "Billy Bob"

在上面的代码中,我们首先定义了一个被代理的目标对象 target,然后声明了包含所有代理操作的 handler 对象,接下来使用 Proxy(target, handler) 创建代理对象 proxy,此后所有使用 proxytarget 属性的访问都会经过 handler 的处理。

1. 抽离校验模块

让我们从一个简单的类型校验开始做起,这个示例演示了如何使用 Proxy 保障数据类型的准确性:

let numericDataStore = {  count: 0,amount: 1234,total: 14
};numericDataStore = new Proxy(numericDataStore, {  set(target, key, value, proxy) {if (typeof value !== 'number') {throw Error("Properties in numericDataStore can only be numbers");}return Reflect.set(target, key, value, proxy);}
});// 抛出错误,因为 "foo" 不是数值
numericDataStore.count = "foo";// 赋值成功
numericDataStore.count = 333;

如果要直接为对象的所有属性开发一个校验器可能很快就会让代码结构变得臃肿,使用 Proxy 则可以将校验器从核心逻辑分离出来自成一体:

function createValidator(target, validator) {  return new Proxy(target, {_validator: validator,set(target, key, value, proxy) {if (target.hasOwnProperty(key)) {let validator = this._validator[key];if (!!validator(value)) {return Reflect.set(target, key, value, proxy);} else {throw Error(`Cannot set ${key} to ${value}. Invalid.`);}} else {throw Error(`${key} is not a valid property`)}}});
}const personValidators = {  name(val) {return typeof val === 'string';},age(val) {return typeof age === 'number' && age > 18;}
}
class Person {  constructor(name, age) {this.name = name;this.age = age;return createValidator(this, personValidators);}
}const bill = new Person('Bill', 25);// 以下操作都会报错
bill.name = 0;
bill.age = 'Bill';
bill.age = 15;  

通过校验器和主逻辑的分离,你可以无限扩展 personValidators 校验器的内容,而不会对相关的类或函数造成直接破坏。更复杂一点,我们还可以使用 Proxy 模拟类型检查,检查函数是否接收了类型和数量都正确的参数:

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();
// > You are incorrectly implementing the signature of pickyMethodOne. Check param 1
// > You are incorrectly implementing the signature of pickyMethodOne. Check param 2
// > You are incorrectly implementing the signature of pickyMethodOne. Check param 3obj.pickyMethodTwo("wopdopadoo", {});
// > You are incorrectly implementing the signature of pickyMethodTwo. Check param 1// No warnings logged
obj.pickyMethodOne({}, "a little string", 123);
obj.pickyMethodOne(123, {});

2. 私有属性

在 JavaScript 或其他语言中,大家会约定俗成地在变量名之前添加下划线 _ 来表明这是一个私有属性(并不是真正的私有),但我们无法保证真的没人会去访问或修改它。在下面的代码中,我们声明了一个私有的 apiKey,便于 api 这个对象内部的方法调用,但不希望从外部也能够访问 api._apiKey:

var api = {  _apiKey: '123abc456def',/* mock methods that use this._apiKey */getUsers: function(){}, getUser: function(userId){}, setUser: function(userId, config){}
};// logs '123abc456def';
console.log("An apiKey we want to keep private", api._apiKey);// get and mutate _apiKeys as desired
var apiKey = api._apiKey;
api._apiKey = '987654321';

很显然,约定俗成是没有束缚力的。使用 ES6 Proxy 我们就可以实现真实的私有变量了,下面针对不同的读取方式演示两个不同的私有化方法。第一种方法是使用 set / get 拦截读写请求并返回 undefined:

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';  

第二种方法是使用 has 拦截 in 操作:

var api = {  _apiKey: '123abc456def',getUsers: function(){ }, getUser: function(userId){ }, setUser: function(userId, config){ }
};const RESTRICTED = ['_apiKey'];
api = new Proxy(api, {  has(target, key) {return (RESTRICTED.indexOf(key) > -1) ?false :Reflect.has(target, key);}
});// these log false, and `for in` iterators will ignore _apiKey
console.log("_apiKey" in api);for (var key in api) {  if (api.hasOwnProperty(key) && key === "_apiKey") {console.log("This will never be logged because the proxy obscures _apiKey...")}
}

3. 访问日志

对于那些调用频繁、运行缓慢或占用执行环境资源较多的属性或接口,开发者会希望记录它们的使用情况或性能表现,这个时候就可以使用 Proxy 充当中间件的角色,轻而易举实现日志功能:

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();

4. 预警和拦截

假设你不想让其他开发者删除 noDelete 属性,还想让调用 oldMethod 的开发者了解到这个方法已经被废弃了,或者告诉开发者不要修改 doNotChange 属性,那么就可以使用 Proxy 来实现:

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();

5. 过滤操作

某些操作会非常占用资源,比如传输大文件,这个时候如果文件已经在分块发送了,就不需要在对新的请求作出相应(非绝对),这个时候就可以使用 Proxy 对当请求进行特征检测,并根据特征过滤出哪些是不需要响应的,哪些是需要响应的。下面的代码简单演示了过滤特征的方式,并不是完整代码,相信大家会理解其中的妙处:

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);}}
});

6. 中断代理

Proxy 支持随时取消对 target 的代理,这一操作常用于完全封闭对数据或接口的访问。在下面的示例中,我们使用了 Proxy.revocable 方法创建了可撤销代理的代理对象:

let sensitiveData = { username: 'devbryce' };
const {sensitiveData, revokeAccess} = Proxy.revocable(sensitiveData, handler);
function handleSuspectedHack(){  revokeAccess();
}// logs 'devbryce'
console.log(sensitiveData.username);
handleSuspectedHack();
// TypeError: Revoked
console.log(sensitiveData.username);

Decorator

ES7 中实现的 Decorator,相当于设计模式中的装饰器模式。如果简单地区分 Proxy 和 Decorator 的使用场景,可以概括为:Proxy 的核心作用是控制外界对被代理者内部的访问,Decorator 的核心作用是增强被装饰者的功能。只要在它们核心的使用场景上做好区别,那么像是访问日志这样的功能,虽然本文使用了 Proxy 实现,但也可以使用 Decorator 实现,开发者可以根据项目的需求、团队的规范、自己的偏好自由选择。

[译] 实例解析 ES6 Proxy 使用场景相关推荐

  1. es6 实例:使用Proxy实现观察者模式

    实例:使用Proxy实现观察者模式 观察者模式(Observer mode)指的是函数自动观察数据对象,一旦对象有变化,函数就会自动执行. const person = observable({ na ...

  2. ES6 Proxy和Reflect (上)

    Proxy概述 Proxy用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种"元编程"(meta programming),即对编程语言进行编程. Proxy可以理 ...

  3. es6 Proxy 简介

    Proxy 简介 Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种"元编程"(meta programming),即对编程语言进行编程. Proxy ...

  4. ES6 Proxy和Reflect

    目录 Proxy 概述 基本用法 Proxy 实例方法 1.get(target, propKey, receiver) 2.set(target, propKey, value, receiver) ...

  5. php的延迟绑定,PHP延迟静态绑定使用方法实例解析

    这篇文章主要介绍了PHP延迟静态绑定使用方法实例解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 PHP的继承模型中有一个存在已久的问题,那就是在 ...

  6. Reactor实例解析

    \ 要点 \\ Reactor是一个运行在Java8之上的响应式流框架,它提供了一组响应式风格的API\\t 除了个别API上的区别,它的原理跟RxJava很相似\\t 它是第四代响应式框架,支持操作 ...

  7. ​iOS的界面触摸事件处理机制,然后用一个实例来说明下应用场景.

    2019独角兽企业重金招聘Python工程师标准>>> 主要是记录下iOS的界面触摸事件处理机制,然后用一个实例来说明下应用场景. 一.处理机制 界面响应消息机制分两块,(1)首先在 ...

  8. java 令牌解析_Java编程guava RateLimiter实例解析

    本文主要研究的是Java编程guava RateLimiter的相关内容,具体如下. 场景1 在流量监管中的应用 约定访问速率(CAR)是流量监管常用技术之一,可以应用在端口进和出方向,一般应用在入方 ...

  9. JDK的动态代理深入解析(Proxy,InvocationHandler)(转)

    一.什么是动态代理 动态代理可以提供对另一个对象的访问,同时隐藏实际对象的具体事实.代理一般会实现它所表示的实际对象的接口.代理可以访问实际对象,但是延迟实现实际对象的部分功能,实际对象实现系统的实际 ...

  10. adaboost算法java_Adaboost 算法实例解析

    Adaboost 算法实例解析 1 Adaboost的原理 1.1 Adaboost基本介绍 AdaBoost,是英文"Adaptive Boosting"(自适应增强)的缩写,由 ...

最新文章

  1. 重磅 | 262亿收购LinkedIn,微软甘当接盘侠?
  2. 7月的夏日北京必联网公司一行(图文)
  3. html教程自适应,Html-自适应
  4. linux+arch系统下载,Linux
  5. 利用python爬取房价
  6. 重磅 | 边缘计算核心技术辨析
  7. 线程的特点 java 1615387415
  8. 安卓bmi项目_搭载安卓系统的智能健康一体机:上禾SH-V20
  9. 易语言mysql线程池数量_线程池最佳线程数量到底要如何配置?
  10. 开始学习redhat8,安装redhat8虚拟机
  11. windows live 2012发布文章解决办法??
  12. sqlserver临时表操作
  13. CF1228C. Primes and Multiplication(数学)
  14. 《Algorithms》Java 语言特性
  15. 用存储过程实现的分页程序
  16. 程序员记录每天工作日志软件_程序员日志记录简介
  17. Zemax操作20--柱透镜
  18. 应用,传输层协议和端口对应关系
  19. 3.C++函数调用过程
  20. 物联网应用领域(全)

热门文章

  1. 北大17秋c语言作业,17春北大 03081002-计算机编程语言 作业答案
  2. Python--sort()函数的用法
  3. php 投票评选插件,jQuery_投票系统显示结果--jQuery插件,投票系统是网站中十分常见的 - phpStudy...
  4. mysql 循环_MySQL存储过程中的3种循环【转载】
  5. android 音频播放插件,在android中的listview中实现音频播放器
  6. 从云计算到人工智能 都逃不过被炒作的命运
  7. Java中的内存处理机制和final、static、final static总结
  8. opera mini 7.5安卓改服版
  9. 【Python学习笔记】集合set
  10. bash shell之数组使用