本文深入解析ES6中的Proxy全局对象,阐述其作为中间件拦截对象访问的核心机制。通过类型校验、私有属性模拟、访问日志记录及操作拦截等具体案例,展示Proxy在控制外部访问、增强功能及安全管理方面的实际应用,帮助开发者掌握这一强大特性。
前言
Proxy基础机制与核心概念
在ES6规范中,Proxy是一个全局可用的构造函数,它允许开发者在目标对象与操作行为之间建立一层拦截机制,从而收集并处理各种请求操作,最终返回一个经过处理的对象。尽管ES6中的箭头函数、数组解构和rest参数等特性已广泛普及,但Proxy这类高级特性却较少被日常使用。这主要受限于浏览器兼容性问题,以及开发者对其适用场景理解的深度不足。就个人而言,我非常推崇ES6的Proxy功能,因为它提供了一种简洁且直观的方式,让开发者能够精确控制外部对对象属性的访问行为。下文将首先介绍Proxy的基本用法,随后通过具体实例深入解析其应用场景。
Proxy,顾名思义,其设计理念与软件设计模式中的“代理模式”高度一致。在软件开发中,代理模式通常应用于以下三个主要方面:
1. 监控并记录外部对目标对象的访问行为。
2. 降低函数或类的直接复杂度,通过代理层进行逻辑分发。
3. 在执行具体操作前进行校验,或对所需资源进行统一管理与分配。
在支持Proxy的现代浏览器环境中,它是一个全局对象,可直接调用。Proxy(target, handler) 是一个构造函数,其中 target 参数代表被代理的目标对象,handlder 参数是一个声明了各类拦截操作(trap)的对象。Proxy最终返回一个代理对象。每当外界通过代理对象访问 target 对象的属性时,请求都会先经过 handler 对象的处理。从流程上看,代理对象非常类似于中间件(middleware)。那么Proxy具体可以拦截哪些操作呢?最常见的包括 get(读取属性)和 set(修改属性)等,完整的可拦截操作列表可参考官方文档。此外,Proxy对象还提供了一个 revoke 方法,用于随时注销所有已注册的代理操作。在正式深入介绍Proxy之前,建议读者先了解另一个ES6新增的全局对象——Reflect,它与Proxy紧密配合使用。
基础用法
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。此后,任何通过 proxy 访问 target 对象属性的操作,都会经过 handler 处理器中定义逻辑的处理。
Proxy在数据校验中的应用
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 3
obj.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, {});
利用Proxy实现私有属性
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...")
}
}
Proxy在日志记录与操作拦截中的实战
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);
装饰器模式对比
ES7中引入的Decorator(装饰器)概念,对应于设计模式中的装饰器模式。如果简单区分Proxy和Decorator的使用场景,可以概括为:Proxy的核心作用是控制外界对被代理者内部状态的访问权限,而Decorator的核心作用是动态增强被装饰者的功能。只要在核心使用场景上做好区分,像本文使用的访问日志功能,虽然可以用Proxy实现,同样也可以使用Decorator实现。开发者可根据项目需求、团队规范或个人偏好自由选择。
总结
ES6的Proxy是一个非常实用的特性,看似简单的接口背后蕴含着巨大的潜力。希望本文能为大家学习ES6提供有益的参考和帮助。