代理的应用场景
在代理设计模式中,代理对象充当另一个主体对象的接口。很多人会将代理模式与门面模式混淆,后文会介绍门面模式。这里需要明确的是,代理模式与门面模式有着本质区别:门面模式的核心是简化接口,将复杂逻辑封装在内部,提供更便捷的调用方式;而代理的核心作用是保护和控制调用者对主体对象的访问。代理会拦截所有或部分主体对象的操作,有时还会增强或补充其行为。
延迟初始化与缓存
由于代理充当了主体对象的保护层,可以有效减少客户端对真实主体对象的无效访问。这就像公司里的销售:收到客户需求后,不会直接透传给项目组,而是先提供产品手册;只有当客户确认购买后,才会让项目团队实施开发,并将结果交付给客户。
这种模式被称为延迟初始化。代理接受初始化请求,在确认调用者确实需要使用主体之前,不会真正创建或传递请求。客户端发出请求后,代理首先响应,但不会将消息传递给主体;只有当客户端明确需要主体完成工作时,代理才会执行后续操作。
在此基础上,我们来看第二个应用场景。代理除了延迟初始化外,还可以增加缓存层:客户端首次访问时,代理将请求合并转发给主体,缓存结果后分别返回;第二次发起相同请求时,代理直接从缓存读取,无需访问主体。
代理的其他应用场景
作为一门 Web 语言,JavaScript 经常需要处理网络请求。基于上述原理,代理在性能优化方面有着重要作用。
- 数据验证:代理在将输入转发给主体之前,可以对输入内容进行验证,确保无误后再传给后端
- 安全验证:代理可以验证客户端是否有权执行某项操作,只有授权通过后才将请求发送给后端
- 日志记录:代理可以拦截方法调用及相关参数,记录后重新编码
接下来,我们来看代理模式在 JavaScript 中的具体实现方式。
代理的实现方式
代理模式在 JavaScript 中有多种实现方式:
- 组合模式(对象组合或工厂模式)
- 对象增强(猴子补丁)
- ES6 内置 Proxy
组合模式
基于函数式编程的思想,我们应尽量保证主体对象的不变性。组合模式正是创建代理的一种简单而安全的方法——它保持主体不变,不会改变其原始行为。
组合模式的缺点是需要手动代理所有方法,即使只想代理其中一个。此外,若要实现延迟初始化,组合模式几乎是唯一选择。
class Calculator {
constructor() { /*...*/ }
plus() { /*...*/ }
minus() { /*...*/ }
}
class ProxyCalculator {
constructor(calculator) {
this.calculator = calculator
}
plus() { return this.calculator.divide() }
minus() { return this.calculator.multiply() }
}
const calculator = new Calculator();
const proxyCalculator = new ProxyCalculator(calculator);也可以使用工厂函数创建代理:
function factoryProxyCalculator(calculator) {
return {
plus() { return calculator.divide() },
minus() { return calculator.multiply() }
}
}
const proxyCalculator = factoryProxyCalculator(calculator);对象增强
对象增强也叫猴子补丁(Monkey Patching)。它的优点是不需要代理所有方法,但最大的问题是改变了主体对象。
这种方式确实简化了代理的创建,但会造成函数式编程中所说的"副作用",因为主体对象失去了不可变性。
function patchingCalculator(calculator) {
const plusOrig = calculator.plus
calculator.plus = () => {
// 额外逻辑
return plusOrig.apply(calculator)
}
return calculator
}
const safeCalculator = patchingCalculator(calculator);内置 Proxy
ES6 引入了 JavaScript 内置的 Proxy,它结合了组合模式和对象增强的优点:既不需要手动代理所有方法,也不会改变主体对象,保持了其不变性。
唯一的缺点是几乎没有 polyfill,使用时需要考虑浏览器兼容性问题。
const handler = {
get(target, property) {
if (property === 'plus') {
return function() {
// 额外逻辑
return target.divide();
}
}
return target[property]
}
}
const proxyCalculator = new Proxy(calculator, handler);Vue 如何用代理实现响应式编程
在讲解单例模式时,我们提到了 Redux 和 reducer,可以看到三方库是如何在传统面向对象模式中融入函数式编程思想的。今天我们来深入剖析 Vue.js 的状态管理思想。
回到核心问题:Vue.js 是如何用代理实现响应式编程的?
Vue.js 通过代理创建了一种 Change Observer(变化观察者) 设计模式。
Vue.js 最显著的特点之一是无侵入的响应式系统。组件状态是响应式 JavaScript 对象,修改后 UI 会自动更新。
这就像使用 Excel:如果在 A2 单元格设置公式 =A0 + A1,修改 A0 或 A1 时,A2 会自动更新。这就是函数式编程中所说的副作用(side effect)。

命令式编程中,副作用并不存在:
let A0 = 1;
let A1 = 2;
let A2 = A0 + A1;
console.log(A2); // 3
A0 = 2;
console.log(A2); // 仍为 3而响应式编程是一种基于声明式的范式。当依赖变化时,自动更新派生值:
let A2;
function update() {
A2 = A0 + A1;
}
whenDepsChange(update); // 伪代码:依赖变化时执行JavaScript 没有内置的 whenDepsChange 来跟踪局部变量的读写。Vue.js 的做法是拦截对象属性的读写。
JavaScript 中有两种拦截方式:getter/setter 和 Proxies。由于浏览器支持限制,Vue 2 仅使用 getter/setter;Vue 3 则使用 Proxies 处理响应式对象,用 getter/setter 处理 ref。
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),如 apply、get、set、has 等。这些方法会在代理实例执行相应操作时自动调用。
import { reactive, computed } from 'vue'
const A0 = reactive(0);
const A1 = reactive(1);
const A2 = computed(() => A0.value + A1.value);
A0.value = 2; // 自动触发更新延伸:Proxy 还能做什么
除了作为代理,JavaScript 内置的 Proxy 还有很多妙用,基于其拦截和定制化特点,广泛应用于:
- 对象虚拟化(Object Virtualization)
- 运算符重载(Operator Overloading)
- 元编程(Meta Programming)
对象虚拟化
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运算符重载
const obj = new Proxy({}, {
get(target, key, receiver) {
console.log(`获取 ${key}!`);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
console.log(`设置 ${key}!`);
return Reflect.set(target, key, value, receiver);
}
});
obj.count = 1; // 输出:设置 count!
obj.count;
// 输出:获取 count!
// 输出:设置 count!
// 输出:1Proxy 的强大之处还在于元编程的实现。
总结
从 Vue.js 基于代理的 Change Observer 模式可以看出:任何设计模式都不是绝对的,它们可以互相结合,形成新的模式来解决实际问题。