代理的应用场景
代理设计模式:
一个代理对象充当另一个主体对象的接口。
与门面模式做对比:
代理模式与门面模式不同,门面模式最主要的功能是简化了接口的设计,把复杂的逻辑实现隐藏在背后,把不同的方法调用结合成更便捷的方法提供出来。
代理对象在调用者和主体对象之间,主要起到的作用是保护和控制调用者对主体对象的访问。代理会拦截所有或部分要在主体对象上执行的操作,有时会增强或补充它的行为。
如上图所示,一般代理和主体具有相同的接口,这对调用者来说是透明的。代理将每个操作转发给主体,通过额外的预处理或后处理增强其行为。这种模式可能看起来像“二道贩子”,但存在即合理,代理特别是在性能优化方面还是起到了很大作用的。
延迟初始化和缓存
代理充当了主体对象的保护作用,减少了客户端对代理背后真实主体无效的消耗。
代理除了起到延迟初始化的作用外,还可以增加一层缓存。
代理的其它应用场景
比如数据验证,代理可以在将输入转发给主体之前对输入的内容进行验证,确保无误后,再传给后端。除此之外,代理模式也可以用于安全验证,代理可以用来验证客户端是否被授权执行操作,只有在检查结果为肯定的情况下,才将请求发送给后端。
代理还有一个应用场景是日志记录,代理可以通过拦截方法调用和相关参数,重新编码。另外,它还可以获取远程对象并放到本地。
代理的实现方式
代理模式在 JavaScript 中有很多种实现方式。其中包含了:
- 对象组合或对象字面量加工厂模式;
- 对象增强;
- 使用从 ES6 开始自带的内置的 Proxy。这几种方式分别有它们的优劣势。
组合模式
基于函数式编程的思想,组合可以被认为是创建代理的一种简单而安全的方法,因为它使主体保持不变,从而不会改变其原始行为。它唯一的缺点是我们必须手动 delegate 所有方法,即使我们只想代理其中的一个方法。
class Calculator {
constructor () {
/*...*/
}
plus () { /*...*/ }
minus () { /*...*/ }
}
class ProxyCalculator {
constructor (calculator) {
this.calculator = calculator
}
// 代理的方法
plus () { return this.calculator.divide() }
minus () { return this.calculator.multiply() }
}
var calculator = new Calculator();
var proxyCalculator = new ProxyCalculator(calculator);
基于组合的思路用工厂函数来做代理创建
function factoryProxyCalculator (calculator) {
return {
// 代理的方法
plus () { return calculator.divide() },
minus () { return calculator.multiply() }
}
}
var calculator = new Calculator();
var proxyCalculator = new factoryProxyCalculator(calculator);
对象增强
对象增强还有一个名字叫猴子补丁(Monkey Patching)。
对于对象增强来说,它的优点就是不需要 delegate 所有方法。但是它最大的问题是改变了主体对象。用这种方式确实是简化了代理创建的工作,但弊端是会造成函数式编程思想中的“副作用”,因为在这里,主体不再具有不可变性。
function patchingCalculator (calculator) {
var plusOrig = calculator.plus
calculator.plus = () => {
// 额外的逻辑
// 委托给主体
return plusOrig.apply(calculator)
}
return calculator
}
var calculator = new Calculator();
var safeCalculator = patchingCalculator(calculator);
内置 Proxy
ES6 内置的 Proxy
从 ES6 之后,JavaScript 便支持了 Proxy。它结合了对象组合和对象增强各自的优点,我们既不需要手动的去 delegate 所有的方法,也不会改变主体对象,保持了主体对象的不变性。但是它也有一个缺点,就是它几乎没有 polyfill。也就是说,如果使用内置的代理,就要考虑在兼容性上做出一定的牺牲。真的是鱼和熊掌不能兼得。
var ProxyCalculatorHandler = {
get: (target, property) => {
if (property === 'plus') {
// 代理的方法
return function () {
// 额外的逻辑
// 委托给主体
return target.divide();
}
}
// 委托的方法和属性
return target[property]
}
}
var calculator = new Calculator();
var proxyCalculator = new Proxy(calculator, ProxyCalculatorHandler);
VUE 如何用代理实现响应式编程
Vue.js 通过代理创建了一种 Change Obsverver 的设计模式。
Vue.js 最显着的特点之一是无侵入的反应系统(unobtrusive reactivity system)。
组件状态是响应式 JavaScript 对象,当被修改时,UI 会更新。就像我们使用 Excel 时,如果我们在 A2 这个格子里设置了一个 A0 和 A1 相加的公式 “= A0 + A1”的话,当我们改动 A0 或 A1 的值的时候,A2 也会随之变化。这也是我们在前面说过很多次的在函数式编程思想中的副作用(side effect)。
在 JavaScript 中,如果我们用命令式编程的方式, 可以看到这种副作用是不存在的。
var A0 = 1;
var A1 = 2;
var A2 = A0 + A1;
console.log(A2) // 返回是 3
A0 = 2;
console.log(A2) // 返回仍然是 3
但响应式编程(Reactive Programming)是一种基于声明式编程的范式。如果要做到响应式编程,我们就会需要下面示例中这样一个 update 的更新功能。
var A2;
function update() {
A2 = A0 + A1;
}
whenDepsChange(update);
在 JavaScript 中没有 whenDepsChange 这样的机制可以跟踪局部变量的读取和写入 。Vue.js 能做的,是拦截对象属性的读写。
Vue 2 仅使用 getter/setter。
在 Vue 3 中,Proxies 用于响应式对象,getter/setter 用于通过属性获取元素的 refs。
function reactive(obj) {
return new Proxy(obj, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, value) {
target[key] = value
trigger(target, key)
}
})
}
handler 里包含一系列具有预定义名称的可选方法了,称为陷阱方法(trap methods)
延伸:Proxy 还可以用于哪些场景
JavaScript 内置的 Proxy 除了作为代理以外,还有很多作用。基于它的拦截和定制化的特点,Proxy 也广泛用于对象虚拟化(object virtualization)、运算符重载(operator overloading)和最近很火的元编程(meta programming)。这里我们不用伪代码,换上一些简单的真代码,看看陷阱方法(trap methods)的强大之处。
对象虚拟化
下面的例子中的 oddNumArr 单数数组就是一个虚拟的对象。我们可以查看一个单双数是不是在单数数组里,我们也可以获取一个单数,但是实际上这个数组里并没有储存任何数据。
const oddNumArr = new Proxy([], {
get: (target, index) => index % 2 === 1 ? index : Number(index)+1,
has: (target, number) => number % 2 === 1
})
console.log(4 in oddNumArr) // false
console.log(7 in oddNumArr) // true
console.log(oddNumArr[15]) // 15
console.log(oddNumArr[16]) // 17
运算符重载
运算符重载就是对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型。比如在下面的例子中,我们就是通过重载“.”这个符号,所以在执行 obj.count 时,我们看到它同时返回了拦截 get 和 set 自定义的方法,以及返回了计数的结果。
var obj = new Proxy({}, {
get: function (target, key, receiver) {
console.log(`获取 ${key}!`);
return Reflect.get(target, key, receiver);
},
set: function (target, key, value, receiver) {
console.log(`设置 ${key}!`);
return Reflect.set(target, key, value, receiver);
}
});
obj.count = 1; // 返回:设置 count!
obj.count;
// 返回:获取 count!
// 返回:设置 count!
// 返回:1
此文章为2月Day2学习笔记,内容来源于极客时间《Jvascript进阶实战课》,大家共同进步💪💪
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。