JavaScript 中代理模式的威力
我在职业生涯后期学到的一个更有趣的模式是Proxy
。
在查找代理模式的示例时,你可能会经常看到不同的实现变体。这是因为代理并不局限于一种用例。一个代理可能充当验证器,而另一个代理可能更注重提升性能等等。
其理念是,通过利用代理,我们包装现有对象,使其功能与原始对象相同,其方法(甚至属性)完全相同,直到我们在调用包装函数之前在包装方法内部添加其他逻辑。这是一个完全隐藏于外部世界的过程,并且此调用对调用者而言始终相同。
换句话说,代理位于对象的客户端和实际对象之间。它可以选择充当“保护者”,也可以添加自定义逻辑(例如缓存),而调用者对此毫不知情。因此,它有时也被称为中介者 (Mediator)。有些人也可能将其归类为装饰者模式的另一种形式,但两者之间存在一些区别。
在这篇文章中,我们将介绍 JavaScript 中代理设计模式的强大功能,并通过几个示例来说明它对您的下一个应用程序有何益处。
由于 JavaScript 本身添加了一个实现该模式的类,因此在几个原始实现之后,Proxy
我们将直接使用该类来演示该模式。Proxy
装饰器与代理的区别
在装饰器模式中,装饰器的主要职责是增强它所包装(或“装饰”)的对象,而代理具有更多的可访问性并控制对象。
代理可以选择增强它所包装的对象或以其他方式控制它,例如限制外界的访问,但装饰器则会通知并应用增强功能。
责任方面的区别显而易见。工程师通常使用装饰器来添加新行为,或者将其作为旧类或遗留类的适配器,返回一个客户端可能知道但并不关心的增强接口。代理通常旨在返回相同的接口,客户端可以假设它正在处理相同的对象,并且没有修改过。
验证器/助手
我将在这里展示的代理模式的第一个实现是一个验证器。
此示例展示了该模式的实现,它有助于验证输入并防止属性被设置为错误的数据类型。请记住,调用者必须始终假设它正在处理原始对象,因此代理不得更改其包装对象的签名或接口:
class Pop {
constructor(...items) {
this.id = 1
}
}
const withValidator = (obj, field, validate) => {
let value = obj[field]
Object.defineProperty(obj, field, {
get() {
return value
},
set(newValue) {
const errMsg = validate(newValue)
if (errMsg) throw new Error(errMsg)
value = newValue
},
})
return obj
}
let mello = new Pop(1, 2, 3)
mello = withValidator(mello, 'id', (newId) => {
if (typeof newId !== 'number') {
return `The id ${newId} is not a number. Received ${typeof newId} instead`
}
})
mello.id = '3'
此示例显示了一个简单的助手,它验证对象的字段,TypeError
当验证失败时抛出异常。
代理拥有属性的所有权,getter
并选择允许或拒绝尝试设置的值。setter
id
在Proxy
课堂上可以用类似这样的方式实现:
const withValidator = (obj, field, validate) => {
return new Proxy(obj, {
set(target, prop, newValue) {
if (prop === field) {
const errMsg = validate(newValue)
if (errMsg) throw new TypeError(errMsg)
target[prop] = newValue
}
},
})
}
let mello = new Pop(1, 2, 3)
mello = withValidator(mello, 'id', (newId) => {
if (typeof newId !== 'number') {
return `The id ${newId} is not a number. Received ${typeof newId} instead`
}
})
mello.id = '3'
验证器运行完美:
TypeError: The id 3 is not a number. Received string instead
剪贴板 Polyfill
本节将介绍如何使用代理服务器支持旧版浏览器,将文本复制到用户剪贴板,前提是浏览器必须支持该Navigator.clipboard
API。如果浏览器不支持,则将回退到使用代理服务器execCommand
复制选定内容。
同样,客户端将始终假定它调用方法的对象是原始对象,并且只知道它正在调用所述方法:
const withClipboardPolyfill = (obj, prop, cond, copyFnIfCond) => {
const copyToClipboard = (str) => {
if (cond()) {
copyFnIfCond()
} else {
const textarea = document.createElement('textarea')
textarea.value = str
textarea.style.visibility = 'hidden'
document.body.appendChild(textarea)
textarea.select()
document.execCommand('copy')
document.body.removeChild(textarea)
}
}
obj[prop] = copyToClipboard
return obj
}
const api = (function () {
const o = {
copyToClipboard(str) {
return navigator.clipboard.writeText(str)
},
}
return o
})()
let copyBtn = document.createElement('button')
copyBtn.id = 'copy-to-clipboard'
document.body.appendChild(copyBtn)
copyBtn.onclick = api.copyToClipboard
copyBtn = withClipboardPolyfill(
copyBtn,
'onclick',
() => 'clipboard' in navigator,
api.copyToClipboard,
)
copyBtn.click()
你可能会问,在这种情况下使用代理有什么意义,而不是直接在实际copyToClipboard
函数内部硬编码实现。如果我们使用代理,我们可以将其作为独立函数重用,并通过控制反转自由地更改实现。
使用此策略的另一个好处是我们不会修改原始函数。
缓存器(增强性能)
缓存可以在许多不同的场景中采用多种形式。例如,http 请求的“重新验证期间过期”机制、 nginx 内容缓存、CPU 缓存、延迟加载缓存、memoization 缓存等等。
在 JavaScript 中我们还可以借助代理实现缓存。
为了在不直接使用类的情况下实现代理模式,Proxy
我们可以执行以下操作:
const simpleHash = (str) =>
str.split('').reduce((acc, str) => (acc += str.charCodeAt(0)), '')
const withMemoization = (obj, prop) => {
const origFn = obj[prop]
const cache = {}
const fn = (...args) => {
const hash = simpleHash(args.map((arg) => String(arg)).join(''))
if (!cache[hash]) cache[hash] = origFn(...args)
return cache[hash]
}
Object.defineProperty(obj, prop, {
get() {
return fn
},
})
return obj
}
const sayHelloFns = {
prefixWithHello(str) {
return `[hello] ${str}`
},
}
const enhancedApi = withMemoization(sayHelloFns, 'prefixWithHello')
enhancedApi.prefixWithHello('mike')
enhancedApi.prefixWithHello('sally')
enhancedApi.prefixWithHello('mike the giant')
enhancedApi.prefixWithHello('sally the little')
enhancedApi.prefixWithHello('lord of the rings')
enhancedApi.prefixWithHello('lord of the rings')
enhancedApi.prefixWithHello('lord of the rings')
enhancedApi.prefixWithHello('lord of the rings')
enhancedApi.prefixWithHello('lord of the rings')
缓存:
{
"109105107101": "[hello] mike",
"11597108108121": "[hello] sally",
"109105107101321161041013210310597110116": "[hello] mike the giant",
"115971081081213211610410132108105116116108101": "[hello] sally the little",
"108111114100321111023211610410132114105110103115": "[hello] lord of the rings"
}
直接在类中实现这一点Proxy
很简单:
const withMemoization = (obj, prop) => {
const origFn = obj[prop]
const cache = {}
const fn = (...args) => {
const hash = simpleHash(args.map((arg) => String(arg)).join(''))
if (!cache[hash]) cache[hash] = origFn(...args)
return cache[hash]
}
return new Proxy(obj, {
get(target, key) {
if (key === prop) {
return fn
}
return target[key]
},
})
}
班级Proxy
我们已经在几个基本的代理模式实现中看到了持久化模式,而不是直接使用Proxy
类。由于 JavaScript 直接Proxy
以对象的形式提供,因此本文的其余部分将以此为方便。
所有剩余的示例都可以在没有的情况下实现Proxy
,但我们将重点关注类语法,因为它更简洁,更容易使用,特别是为了这篇文章。
代理到单例
如果你从未听说过单例模式 (Singleton),它是另一种设计模式,它确保在应用程序的整个生命周期内,如果某个对象已经实例化,则该对象将被返回并重用。在实践中,你很可能会看到它被用作全局变量。
例如,如果我们正在编写一个 MMORPG 游戏,并且我们有三个类Equipment
,,Person
并且Warrior
只能存在一个,那么我们可以在 实例化类时Warrior
使用construct
第二个参数内的处理程序方法:Proxy
Warrior
class Equipment {
constructor(equipmentName, type, props) {
this.id = `_${Math.random().toString(36).substring(2, 16)}`
this.name = equipmentName
this.type = type
this.props = props
}
}
class Person {
constructor(name) {
this.hp = 100
this.name = name
this.equipments = {
defense: {},
offense: {},
}
}
attack(target) {
target.hp -= 5
const weapons = Object.values(this.equipments.offense)
if (weapons.length) {
for (const weapon of weapons) {
console.log({ weapon })
target.hp -= weapon.props.damage
}
}
}
equip(equipment) {
this.equipments[equipment.type][equipment.id] = equipment
}
}
class Warrior extends Person {
constructor() {
super(...arguments)
}
bash(target) {
target.hp -= 15
}
}
function useSingleton(_Constructor) {
let _warrior
return new Proxy(_Constructor, {
construct(target, args, newTarget) {
if (!_warrior) _warrior = new Warrior(...args)
return _warrior
},
})
}
const WarriorSingleton = useSingleton(Warrior)
如果我们尝试创建多个实例,Warrior
我们可以确保每次只使用创建的第一个实例:
const mike = new WarriorSingleton('mike')
const bob = new WarriorSingleton('bob')
const sally = new WarriorSingleton('sally')
console.log(mike)
console.log(bob)
console.log(sally)
结果:
Warrior {
hp: 100,
name: 'mike',
equipments: { defense: {}, offense: {} }
}
Warrior {
hp: 100,
name: 'mike',
equipments: { defense: {}, offense: {} }
}
Warrior {
hp: 100,
name: 'mike',
equipments: { defense: {}, offense: {} }
}
饼干窃贼
在本节中,我们将演示一个示例,使用 来Proxy
阻止 cookie 列表的更改。这将阻止原始对象被更改,并且更改器(CookieStealer
)将假定他们的恶意操作成功了。
让我们看一下这个例子:
class Food {
constructor(name, points) {
this.name = name
this.points = points
}
}
class Cookie extends Food {
constructor() {
super(...arguments)
}
setFlavor(flavor) {
this.flavor = flavor
}
}
class Human {
constructor() {
this.foods = []
}
saveFood(food) {
this.foods.push(food)
}
eat(food) {
if (this.foods.includes(food)) {
const foodToEat = this.foods.splice(this.foods.indexOf(food), 1)[0]
this.hp += foodToEat.points
}
}
}
const apple = new Food('apple', 2)
const banana = new Food('banana', 2)
const chocolateChipCookie = new Cookie('cookie', 2)
const sugarCookie = new Cookie('cookie', 2)
const butterCookie = new Cookie('cookie', 3)
const bakingSodaCookie = new Cookie('cookie', 3)
const fruityCookie = new Cookie('cookie', 5)
chocolateChipCookie.setFlavor('chocolateChip')
sugarCookie.setFlavor('sugar')
butterCookie.setFlavor('butter')
bakingSodaCookie.setFlavor('bakingSoda')
fruityCookie.setFlavor('fruity')
const george = new Human()
george.saveFood(apple)
george.saveFood(banana)
george.saveFood(chocolateChipCookie)
george.saveFood(sugarCookie)
george.saveFood(butterCookie)
george.saveFood(bakingSodaCookie)
george.saveFood(fruityCookie)
console.log(george)
乔治的食物:
{
foods: [
Food { name: 'apple', points: 2 },
Food { name: 'banana', points: 2 },
Cookie { name: 'cookie', points: 2, flavor: 'chocolateChip' },
Cookie { name: 'cookie', points: 2, flavor: 'sugar' },
Cookie { name: 'cookie', points: 3, flavor: 'butter' },
Cookie { name: 'cookie', points: 3, flavor: 'bakingSoda' },
Cookie { name: 'cookie', points: 5, flavor: 'fruity' }
]
}
我们george
使用该类进行了实例化Human
,并在其存储中添加了 7 种食物。乔治很高兴他即将吃水果和饼干。他对饼干尤其兴奋,因为他同时获得了他最喜欢的口味,很快就会狼吞虎咽地吃掉它们,满足他对饼干的渴望。
但是,有一个问题:
const CookieStealer = (function () {
const myCookiesMuahahaha = []
return {
get cookies() {
return myCookiesMuahahaha
},
isCookie(obj) {
return obj instanceof Cookie
},
stealCookies(person) {
let indexOfCookie = person.foods.findIndex(this.isCookie)
while (indexOfCookie !== -1) {
const food = person.foods[indexOfCookie]
if (this.isCookie(food)) {
const stolenCookie = person.foods.splice(indexOfCookie, 1)[0]
myCookiesMuahahaha.push(stolenCookie)
}
indexOfCookie = person.foods.findIndex(this.isCookie)
}
},
}
})()
CookieStealer.stealCookies(george)
突然CookieStealer
出现偷走了他的饼干。CookieStealer
现在他的存储里有5块饼干:
[
Cookie { name: 'cookie', points: 2, flavor: 'chocolateChip' },
Cookie { name: 'cookie', points: 2, flavor: 'sugar' },
Cookie { name: 'cookie', points: 3, flavor: 'butter' },
Cookie { name: 'cookie', points: 3, flavor: 'bakingSoda' },
Cookie { name: 'cookie', points: 5, flavor: 'fruity' }
]
乔治:
Human {
foods: [
Food { name: 'apple', points: 2 },
Food { name: 'banana', points: 2 }
]
}
如果我们倒回去,让我们的救世主Superman
应用他的一种方法来实现这个Proxy
模式来阻止CookieStealer
他的邪恶行为,那么我们的问题就解决了:
class Superman {
protectFromCookieStealers(obj, key) {
let realFoods = obj[key]
let fakeFoods = [...realFoods]
return new Proxy(obj, {
get(target, prop) {
if (key === prop) {
fakeFoods = [...fakeFoods]
Object.defineProperty(fakeFoods, 'splice', {
get() {
return function fakeSplice(...[index, removeCount]) {
fakeFoods = [...fakeFoods]
return fakeFoods.splice(index, removeCount)
}
},
})
return fakeFoods
}
return target[prop]
},
})
}
}
const superman = new Superman()
const slickGeorge = superman.protectFromCookieStealers(george, 'foods')
我们的朋友superman
很幸运,他恰好拥有一个protectFromCookieStealers
利用 的力量Proxy
伪造饼干清单的人!他把包含乔治饼干的真正食物藏了起来,不让CookieStealer
。CookieStealer
他继续实施他的邪恶计划,似乎被骗了,以为他已经拿到了饼干:
CookieStealer.stealCookies(slickGeorge)
console.log(CookieStealer.cookies)
他CookieStealer
带着储藏室里的饼干离开了,并认为他已经逃脱了惩罚:
[
Cookie { name: 'cookie', points: 2, flavor: 'chocolateChip' },
Cookie { name: 'cookie', points: 2, flavor: 'sugar' },
Cookie { name: 'cookie', points: 3, flavor: 'butter' },
Cookie { name: 'cookie', points: 3, flavor: 'bakingSoda' },
Cookie { name: 'cookie', points: 5, flavor: 'fruity' }
]
他不知道自己被超人欺骗了,那些是假饼干!george
由于超人的力量将Proxy
他从邪恶的黑暗中拯救出来,他的饼干仍然完好无损:
console.log(slickGeorge)
Human {
foods: [
Food { name: 'apple', points: 2 },
Food { name: 'banana', points: 2 },
Cookie { name: 'cookie', points: 2, flavor: 'chocolateChip' },
Cookie { name: 'cookie', points: 2, flavor: 'sugar' },
Cookie { name: 'cookie', points: 3, flavor: 'butter' },
Cookie { name: 'cookie', points: 3, flavor: 'bakingSoda' },
Cookie { name: 'cookie', points: 5, flavor: 'fruity' }
]
}
结论
我希望这有助于阐明代理模式以及如何使用Proxy
JavaScript 中现在内置的类来利用这个概念。
这篇文章到此结束 :) 我希望你发现这篇文章对你有帮助,并确保在 Medium 上关注我,以便查看以后的文章!
在Medium上找到我
文章来源:https://dev.to/jsmanifest/the-power-of-proxy-pattern-in-javscript-6fp