代理模式(Proxy Pattern
)是程序设计中的一种设计模式。javascript
在现实生活中,proxy
是一个被受权表明其余人的人。好比,许多州容许代理投票,这意味着你能够受权他人在选举中表明你投票。java
你极可能据说过proxy服务器,它会接收来自你这的全部流量,表明你发送给另外一端,并把响应返回给你。当你不但愿请求的目的地知道你请求的具体来源时,使用proxy服务器就颇有用了。全部的目标服务器看到的只是来自proxy服务器的请求。es6
再接近本文的主题一些,这种类型的代理和ES6 proxy要作的就很相似了,涉及到使用类(B)去包装类(A)并拦截/控制对(A)的访问。ajax
当你想进行如下操做时proxy模式一般会颇有用:编程
当一个复杂对象的多份副本须存在时,代理模式能够结合享元模式以减小内存用量。典型做法是建立一个复杂对象及多个代理者,每一个代理者会引用到本来的复杂对象。而做用在代理者的运算会转送到本来对象。一旦全部的代理者都不存在时,复杂对象会被移除。后端
上面是维基百科中对代理模式的一个总体的定义.而在JavaScript中代理模式的具体表现形式就是ES6中的新增对象---Proxy设计模式
ES6所提供Proxy
构造函数可以让咱们轻松的使用代理模式:api
let proxy = new Proxy(target, handler);
复制代码
Proxy构造函数传入两个参数,第一个参数target表示所要代理的对象,第二个参数handler也是一个对象用来设置对所代理的对象的行为。若是想知道Proxy的具体使用方法,可参考阮一峰的《 ECMAScript入门 - Proxy 》。缓存
Proxy构造器能够在全局对象上访问到。经过它,你能够有效的拦截针对对象的各类操做,收集访问的信息,并按你的意愿返回任何值。从这个角度来讲,proxy和中间件有不少类似之处。安全
let dataStore = {
name: 'Billy Bob',
age: 15
};
let handler = {
get(target, key, proxy) {
const today = new Date();
console.log(`GET request made for ${key} at ${today}`);
return Reflect.get(target, key, proxy);
}
}
dataStore = new Proxy(dataStore, handler);
// 这会执行咱们的拦截逻辑,记录请求并把值赋给`name`变量
const name = dataStore.name;
复制代码
具体来讲,proxy容许你拦截许多对象上经常使用的方法和属性,最多见的有get
,set
,apply
(针对函数)和construct
(针对使用new关键字调用的构造函数)。关于使用proxy能够拦截的方法的完整列表,请参考规范。Proxy还能够配置成随时中止接受请求,有效的取消全部针对被代理的目标对象的访问。这能够经过一个revoke
方法实现。
一个把Proxy用于验证的例子,验证一个数据源中的全部属性都是同一类型。下面的例子中咱们要确保每次给numericDataStore
数据源设置一个属性时,它的值必须是数字。
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);
}
});
// 这会抛出异常
numericDataStore.count = "foo";
// 这会设置成功
numericDataStore.count = 333;
复制代码
这颇有意思,但有多大的可能性你会建立一个这样的对象呢?确定不会。。。
若是你想为一个对象上的部分或所有属性编写自定义的校验规则,代码可能会更复杂一些,但我很是喜欢Proxy能够帮你把校验代码与核心代码分离开这一点。难道只有我讨厌把校验代码和方法或类混在一块儿吗?
// 定义一个接收自定义校验规则并返回一个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;
复制代码
经过这种方式,你就能够无限的扩展校验规则而不用修改类或方法。
再说一个和校验有关的点子。假设你想检查传给一个方法的参数并在传入的参数与函数签名不符时输出一些有用的帮助信息。你能够经过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 3
obj.pickyMethodTwo("wopdopadoo", {});
// > You are incorrectly implementing the signature of pickyMethodTwo. Check param 1
// 不会输出警告信息
obj.pickyMethodOne({}, "a little string", 123);
obj.pickyMethodOne(123, {});
复制代码
在看一个表单验证的例子。Proxy构造函数第二个参数中的set
方法,能够很方便的验证向一个对象的传值。咱们以一个传统的登录表单举例,该表单对象有两个属性,分别是account
和password
,每一个属性值都有一个简单和其属性名对应的验证方法,验证规则以下:
// 表单对象
const userForm = {
account: '',
password: '',
}
// 验证方法
const validators = {
account(value) {
// account 只容许为中文
const re = /^[\u4e00-\u9fa5]+$/;
return {
valid: re.test(value),
error: '"account" is only allowed to be Chinese'
}
},
password(value) {
// password 的长度应该大于6个字符
return {
valid: value.length >= 6,
error: '"password "should more than 6 character'
}
}
}
复制代码
下面咱们来使用Proxy实现一个通用的表单验证器
const getValidateProxy = (target, validators) => {
return new Proxy(target, {
_validators: validators,
set(target, prop, value) {
if (value === '') {
console.error(`"${prop}" is not allowed to be empty`);
return target[prop] = false;
}
const validResult = this._validators[prop](value);
if(validResult.valid) {
return Reflect.set(target, prop, value);
} else {
console.error(`${validResult.error}`);
return target[prop] = false;
}
}
})
}
const userFormProxy = getValidateProxy(userForm, validators);
userFormProxy.account = '123'; // "account" is only allowed to be Chinese
userFormProxy.password = 'he'; // "password "should more than 6 character
复制代码
咱们调用getValidateProxy
方法去生成了一个代理对象userFormProxy
,该对象在设置属性的时候会根据validators
的验证规则对值进行校验。这咱们使用的是console.error
抛出错误信息,固然咱们也能够加入对DOM的事件来实现页面中的校验提示。
在JavaScript中常见的作法是在属性名以前或以后放一个下划线来标识该属性仅供内部使用。但这并不能阻止其余人读取或修改它。
在下面的例子中,有一个咱们想在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,你能够经过若干方式来实现真实,彻底的私有属性。
首先,你可使用一个proxy来截获针对某个属性的请求并做出限制或是直接返回undefined
。
var api = {
_apiKey: '123abc456def',
/* mock methods that use this._apiKey */
getUsers: function(){ },
getUser: function(userId){ },
setUser: function(userId, config){ }
};
// Add other restricted properties to this array
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);
}
});
// throws an error
console.log(api._apiKey);
// throws an error
api._apiKey = '987654321';
复制代码
你还可使用has
trap来掩盖这个属性的存在。
var api = {
_apiKey: '123abc456def',
/* mock methods that use this._apiKey */
getUsers: function(){ },
getUser: function(userId){ },
setUser: function(userId, config){ }
};
// Add other restricted properties to this array
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...")
}
}
复制代码
针对那些重度依赖资源,执行缓慢或是频繁使用的方法或接口,你可能喜欢统计它们的使用或是性能。Proxy能够很容易的悄悄在后台作到这一点。
注意:你不能仅仅使用apply
trap来拦截方法。任何使用当你要执行某个方法时,你首先须要get这个方法。所以,若是你要拦截一个方法调用,你须要先拦截对该方法的get
操做,而后拦截apply
操做。
let api = {
_apiKey: '123abc456def',
getUsers: function() { /* ... */ },
getUser: function(userId) { /* ... */ },
setUser: function(userId, config) { /* ... */ }
};
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);
};
}
});
// executes apply trap in the background
api.getUsers();
function logMethodAsync(timestamp, method) {
setTimeout(function() {
console.log(`${timestamp} - Logging ${method} request asynchronously.`);
}, 0)
}
复制代码
这很酷,由于你能够记录各类各样的信息而不用修改应用程序的代码或是阻塞代码执行。而且只须要在这些代码的基础上稍事修改就能够记录特性函数的执行性能了。
假设你想阻止其余人删除noDelete
属性,想让调用oldMethod
方法的人知道该方法已经被废弃,或是想阻止其余人修改doNotChange
属性。如下是一种快捷的方法。
let dataStore = {
noDelete: 1235,
oldMethod: function() {/*...*/ },
doNotChange: "tried and true"
};
const NODELETE = ['noDelete'];
const DEPRECATED = ['oldMethod'];
const NOCHANGE = ['doNotChange'];
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();
复制代码
假设你有一个服务器接口返回一个巨大的文件。当前一个请求还在处理中,或是文件正在被下载,又或是文件已经被下载以后你不想该接口被再次请求。代理在这种状况下能够很好的缓冲对服务器的访问并在可能的时候读取缓存,而不是按照用户的要求频繁请求服务器。缓存代理
能够将一些开销很大的方法的运算结果进行缓存,再次调用该函数时,若参数一致,则能够直接返回缓存中的结果,而不用再从新进行运算。例如在采用后端分页的表格时,每次页码改变时须要从新请求后端数据,咱们能够将页码和对应结果进行缓存,当请求同一页时就不用在进行ajax请求而是直接返回缓存中的数据。在这里我会跳过大部分代码,但下面的例子仍是足够向你展现它的工做方式。
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);
}
}
});
复制代码
再列举一个比较好理解的例子
下面咱们以没有通过任何优化的计算斐波那契数列的函数来假设为开销很大的方法,这种递归调用在计算40以上的斐波那契项时就能明显的感到延迟感。
const getFib = (number) => {
if (number <= 2) {
return 1;
} else {
return getFib(number - 1) + getFib(number - 2);
}
}
复制代码
如今咱们来写一个建立缓存代理的工厂函数:
const getCacheProxy = (fn, cache = new Map()) => {
return new Proxy(fn, {
apply(target, context, args) {
const argsString = args.join(' ');
if (cache.has(argsString)) {
// 若是有缓存,直接返回缓存数据
console.log(`输出${args}的缓存结果: ${cache.get(argsString)}`);
return cache.get(argsString);
}
const result = fn(...args);
cache.set(argsString, result);
return result;
}
})
}
const getFibProxy = getCacheProxy(getFib);
getFibProxy(40); // 102334155
getFibProxy(40); // 输出40的缓存结果: 102334155
复制代码
当咱们第二次调用getFibProxy(40)
时,getFib
函数并无被调用,而是直接从cache
中返回了以前被缓存好的计算结果。经过加入缓存代理的方式,getFib
只须要专一于本身计算斐波那契数列的职责,缓存的功能使由Proxy对象实现的。这实现了咱们以前提到的单一职责原则。
Proxy支持随时撤销对目标对象的访问。当你想完全封锁对某些数据或API的访问时(好比,出于安全,认证,性能等缘由),这可能会颇有用。如下是一个使用revocable
方法的简单例子。注意当你使用它时,你不须要对Proxy方法使用new
关键字。
let sensitiveData = {
username: 'devbryce'
};
const {sensitiveData, revokeAccess} = Proxy.revocable(sensitiveData, handler);
function handleSuspectedHack(){
// Don't panic
// Breathe
revokeAccess();
}
// logs 'devbryce'
console.log(sensitiveData.username);
handleSuspectedHack();
// TypeError: Revoked
console.log(sensitiveData.username);
复制代码
好吧,以上就是全部我要讲的内容。我很但愿能听到你在工做中是如何使用Proxy的。
在面向对象的编程中,代理模式的合理使用可以很好的体现下面两条原则:
单一职责原则: 面向对象设计中鼓励将不一样的职责分布到细粒度的对象中,Proxy 在原对象的基础上进行了功能的衍生而又不影响原对象,符合松耦合高内聚的设计理念。
开放-封闭原则:代理能够随时从程序中去掉,而不用对其余部分的代码进行修改,在实际场景中,随着版本的迭代可能会有多种缘由再也不须要代理,那么就能够容易的将代理对象换成原对象的调用
对于代理模式 Proxy 的做用主要体如今三个方面:
一、 拦截和监视外部对对象的访问
二、 下降函数或类的复杂度
三、 在复杂操做前对操做进行校验或对所需资源进行管理
参考: