ES6实战:如何使用Proxy
ES6实战:如何使用Proxy
引言
计算机术语中,代理定位于你和通信方之间。
代理大部分指的是代理服务器,一个在浏览器和存放网页的web服务器之间的设备。
代理服务器能够修改请求和响应。
例如, 它可以缓存经常访问的资源,提供给更多的用户,提高访问效率。
ES6代理定位代码与对象之间。
代理允许执行元编程操作,如,拦截调用监察或改变对象的属性。
下面的术语是和ES6代理有关系的:
目标对象
代理将会虚拟化的原始对象。
可以是js对象, 如: jquery对象或者原生对象, 数组,甚至是其他代理。
处理者对象
实现了代理行为的对象。
追踪函数
处理函数内部定义的函数,当特定属性或方法调用时,提供目标对象的访问。
下面简单示例可以最佳诠释,我们创建一个有三个熟悉的目标对象target
:
const target = {
a: 1,
b: 2,
c: 3
};
我们再创建一个处理者对象,拦截所有get操作,拦截过后返回目标对象属性或数字42。
const handler = {
get: function(target, name) {
return (
name in target ? target[name] : 42
);
}
};
我们创建新代理对象,传入目标对象和处理者对象,这段代码可以和代理进行数据交互,而不是直接访问目标对象本身:
const proxy = new Proxy(target, handler);
console.log(proxy.a); // 1
console.log(proxy.b); // 2
console.log(proxy.c); // 3
console.log(proxy.meaningOfLife); // 42
我们扩展下代理处理者对象,让它只允许设置a-z单字母属性:
const handler = {
get: function(target, name) {
return (name in target ? target[name] : 42);
},
set: function(target, prop, value) {
if (prop.length == 1 && prop >= 'a' && prop <= 'z') {
target[prop] = value;
return true;
} else {
throw new ReferenceError(prop + ' cannot be set');
return false;
}
}
};
const proxy = new Proxy(target, handler);
proxy.a = 10;
proxy.b = 20;
proxy.ABC = 30;
// Exception: ReferenceError: ABC cannot be set
代理捕获类型
我们看到get
和set
生效,可能是最有用的捕获。
但是还有其他可用捕获类型支持代理处理者对象。
-
construct(target, argList)
捕获新对象new操作符的创建。
-
get(target, property)
捕获对象get方法,必须返回属性值
-
set(target, property, value)
捕获对象set方法,必须设置属性值。成功返回true。严格模式中, 返回false会抛出类型错误。
-
deleteProperty(target, property)
捕获针对对象属性的删除操作,必须返回true或false。
-
apply(target, thisArg, argList)
捕获对象函数调用
-
has(target, property)
捕获in操作符,必须返回true或false。
-
ownKeys(target)
捕获
Object.getOwnPropertyNames
, 必须返回枚举对象。 -
getPrototypeOf(target)
捕获
Object.getPrototypeOf
, 必须返回对象原型或空。 -
setPrototypeOf(target, prototype)
捕获
Object.setPrototypeOf
设置原型对象,无返回值。 -
isExtensible(target)
捕获
Object.isExtensible
, 决定对象是否可以添加新属性,必须返回true或false。 -
preventExtensions(target)
捕获
Object.preventExtensions
, 防止对象添加新属性,必须返回true或false. -
getOwnPropertyDescriptor(target, property)
捕获
Object.getOwnPropertyDescriptor
, 返回undefined或属性描述对象,有value
,writable
,get
,set
,configurable
和enumerable
. -
defineProperty(target, property, descriptor)
捕获
Object.defineProperty
, 定义或修改对象属性。必须返回true或false.
如果目标对象属性成功定义返回true,反之false.
代理示例1: 监控
代理允许创建任何通用包装器,无需改变目标对象内部代码。
本例中,我们创建一个监控代理,计算属性的访问次数。
首先,我们需要创建makeProfiler
工厂函数,返回代理对象,保持计数状态:
// 创建监控代理
function makeProfiler(target) {
const
count = {},
handler = {
get: function(target, name) {
if (name in target) {
count[name] = (count[name] || 0) + 1;
return target[name];
}
}
};
return {
proxy: new Proxy(target, handler),
count: count
}
};
我们可以应用代理包装器至任意对象或其他代理。例如:
const myObject = {
h: 'Hello',
w: 'World'
};
// 创建对象代理
const pObj = makeProfiler(myObject);
// 访问属性
console.log(pObj.proxy.h); // Hello
console.log(pObj.proxy.h); // Hello
console.log(pObj.proxy.w); // World
console.log(pObj.count.h); // 2
console.log(pObj.count.w); // 1
这只是个简单示例, 想象如果不用代理,实现几个不同对象属性访问计数需要的代码量。
代理示例2:双向绑定
对象之间可以进行双向绑定。
特别用在js mvc库中, DOM变化时用来更新内部对象。
假设有一个输入框,id是inputname
:
<input type="text" id="inputname" value="" />
还有js对象myUser
, 有id属性指向input。
// #inputname 输入框的内部状态
const myUser = {
id: 'inputname',
name: ''
};
我们的首要目标是用户改变输入框值时,更新myUser
。
这可以通过绑定onchange
事件实现:
inputChange(myUser);
// bind input to object
function inputChange(myObject) {
if (!myObject || !myObject.id) return;
const input = document.getElementById(myObject.id);
input.addEventListener('onchange', function(e) {
myObject.name = input.value;
});
}
我们的下一个目标是更改对象值时更新输入框显示。
原生js实现并简单, 但代理提供一种实现方案:
// 代理处理者
const inputHandler = {
set: function(target, prop, newValue) {
if (prop == 'name' && target.id) {
// 更新对象属性
target[prop] = newValue;
// 更新输入框值
document.getElementById(target.id).value = newValue;
return true;
}
else return false;
}
}
// 创建代理
const myUserProxy = new Proxy(myUser, inputHandler);
// 设置新名称
myUserProxy.name = 'Craig';
console.log(myUserProxy.name); // Craig
console.log(document.getElementById('inputname').value); // Craig
这也许不是最有效率的双向绑定方案,但代理让你可以改变已存在对象行为,无需改变内部代码。
未来示例
Hemanth.HM 在文章JavaScript中的负数数组下标中建议使用代理实现负数数组下标。
例如, arr[-1]
返回最后一个元素。arr[-2]
返回倒数第二个元素,以此类推。
Nicholas C. Zakas在使用ES6 代理创建类型安全的属性
中描述了如何使用代理通过验证新值实现类型安全。
上例中,我们可以查证myUserProxy.name
总是设置成字符串类型,否则抛错。
代理支持
代理的作用或许没有那么显而易见。
但是代理提供了强大的元编程机会。
js作者Brendan Eich,认为代理很好!
目前除了IE11, node环境和现代浏览器都提供proxy支持。
但是记住不是所有浏览器支持所有的捕获。
查询浏览器兼容速查表, 可以知道代理支持情况。
另外可惜的是,无法通过使用polyfill或用Babel转译ES6代理代码实现代理,因为代理强大到没有ES5实现支持。
译者注
-
因译者水平有限,如有错误,欢迎指正交流