原文: Javascript Proxies: Real World Use Case -- Arbaz Siddiqui 译者注, 为了防止出现"鲁棒性"这种因翻译习惯差异导致的混淆, 文中部分术语将不会进行翻译.
在编程术语范畴中, Proxy指的是帮助/替代另一个实体(Entity)完成一系列操作的实体. 一个架设在客户端与服务端之间的Proxy服务器分别充当了客户端的服务端和服务端的客户端. 对于Proxy来说, 它们的任务就是介入收到的请求/调用, 并在处理后传递给其上游. 这些介入允许Proxy添加一些额外的业务逻辑或者改变整个操作的行为.
JavaScript的Proxy从某种意义上来说是相似的. 它处在代码所操作的对象与实际被操作的对象之间进行处理.
根据MDN Web文档
The Proxy is used to define custom behavior for fundamental operations (e.g. property lookup, assignment, enumeration, function invocation, etc). Proxy被用来自定义一些基础层面的操作(例如属性查找, 赋值, 枚举, 函数调用等)
在完成一个Proxy的使用之前, 有三个术语需要我们提前进行了解:
Target就是实际被Proxy操作修改的对象. 它可以是任何一个JavaScript对象.
译者注: 这个地方的翻译说实话有点不太好翻译, 但实际上只要能够理解所谓Traps就是用来重载(代理)Target对应的名字的属性/方法的属性/方法就行
Traps是指那些在Target的属性或者方法被调用时会介入干涉的方法. 有许多定义了的Traps可以被实现(implement)
Handler是一个所有的Traps生存的占位对象. 简单来说, 可以把它当做一个存放且实现各个traps的对象.
我们来看看下面这个例子:
//movie is a target
const movie = {
name: "Pulp Fiction",
director: "Quentin Tarantino"
};
//this is a handler
const handler = {
//get is a trap
get: (target, prop) => {
if (prop === 'director') {
return 'God'
}
return target[prop]
},
set: function (target, prop, value) {
if (prop === 'actor') {
target[prop] = 'John Travolta'
} else {
target[prop] = value
}
}
};
const movieProxy = new Proxy(movie, handler);
console.log(movieProxy.director); //God
movieProxy.actor = "Tim Roth";
movieProxy.actress = "Uma Thurman";
console.log(movieProxy.actor); //John Travolta
console.log(movieProxy.actress); //Uma Thurman
输出如下
God
John Travolta
Uma Thurman
上面这个例子中, movie
就是我们所说的Target. 我们实现了一个拥有set
和get
这两个trap的handler
. 在其中我们添加了两个逻辑: 在访问director
时, get
这个trap会直接返回God
而不是它实际的值; 在对actor
赋值时, set
这个trap会干涉所有的赋值操作, 并在键为actor
时将值改变成John Travlota
.
虽然并不如其他的ES2015的特性那样广为人知, Proxy还是有诸如所有属性的默认值这样的现在看来挺亮眼的用例. 让我们来看看其他的在真实生产环场景中能够利用Proxy的地方.
既然我们已经可以干涉对象的属性赋值过程, 那么我们可以借此来校验我们将要赋予给对象属性的值. 看下面这个例子
const handler = {
set: function (target, prop, value) {
const houses = ['Stark', 'Lannister'];
if (prop === 'house' && !(houses.includes(value))) {
throw new Error(`House ${value} does not belong to allowed ${houses}`)
}
target[prop] = value
}
};
const gotCharacter = new Proxy({}, handler);
gotCharacter.name = "Jamie";
gotCharacter.house = "Lannister";
console.log(gotCharacter);
gotCharacter.name = "Oberyn";
gotCharacter.house = "Martell";
运行结果如下:
{ name: 'Jamie', house: 'Lannister' }
Error: House Martell does not belong to allowed Stark,Lannister
上面这个例子中, 我们严格限制了house
这个属性所能被赋予的值的范围. 只需要创建一个set
的trap, 我们甚至能用这个实现方式来实现一个只读的对象.
我们可以通过Proxy来创建一个在读写属性时的副作用. 出发点在于某些特定的属性被访问或者写入时触发一些函数. 看下面这个例子:
const sendEmail = () => {
console.log("sending email after task completion")
};
const handler = {
set: function (target, prop, value) {
if (prop === 'status' && value === 'complete') {
sendEmail()
}
target[prop] = value
}
};
const tasks = new Proxy({}, handler);
tasks.status = "complete";
运行结果如下:
sending email after task completion
这里我们干涉了status
这个属性的写入. 当写入的值是complete
时, 会触发一个副作用函数. 在Sindre Sorhus的on-change这个包中就一个很Cooooooool的实现.
利用介入干涉对象属性读写的能力, 我们能够创建一个基于内存的缓存. 它只会在值过期前返回值. 看下面这个例子:
const cacheTarget = (target, ttl = 60) => {
const CREATED_AT = Date.now();
const isExpired = () => (Date.now() - CREATED_AT) > (ttl * 1000);
const handler = {
get: (target, prop) => isExpired() ? undefined : target[prop]
};
return new Proxy(target, handler)
};
const cache = cacheTarget({age: 25}, 5);
console.log(cache.age);
setTimeout(() => {
console.log(cache.age)
}, 6 * 1000);
运行结果如下:
25
undefined
这里我们创建了一个函数, 并返回一个Proxy. 在获取target的属性前, 这个Proxy的handler首先会检查target对象是否过期. 基于此, 我们可以针对每个键值都设置一个基于TTLs或者其他机制的过期检查.
虽然Proxy具备一些很神奇的功能, 但在使用时仍然具有一些不得不小心应对的限制:
Proxy很强, 在很大范围内都能够得到应用, 或者被滥用. 这篇文章中我们讨论了什么是Proxy, 如何实现一个Proxy, 几个真实案例中的用例, 以及它的缺陷限制.