JavaScript’te Proxy Modelinin Gücü – JSManifest

Kariyerimin daha sonraki bir aşamasında öğrendiğim daha ilginç kalıplardan biri, Proxy
.
Proxy modelinin örneklerini aradığınızda, genellikle farklı uygulama varyasyonları görebilirsiniz. Bunun nedeni, Proxy’nin tek bir kullanım durumuyla sınırlı olmamasıdır. Bir vekil doğrulayıcı olarak hareket edebilirken diğeri performansı iyileştirme vb. ile daha fazla ilgilenebilir.
Buradaki fikir, bir proxy kullanarak, orijinal ile aynı işlevi gören mevcut nesneleri, yöntemlerinin (hatta özelliklerinin) tamamen aynı olduğu, sarmalanmış yöntemlerin içine ek mantık ekleyene kadar sarmamızdır. sarılmış işlev çağrılmadan önce. Bu tamamen dış dünyaya gizlenmiş bir süreçtir ve bu çağrı arayan kişiye her zaman aynı görünecektir.
Başka bir deyişle, proxy, bir nesnenin istemcisi ile gerçek nesnenin kendisi arasında oturur. Burası bir “koruyucu” olarak hareket etmeyi veya aşağıdaki gibi özel bir mantık eklemeyi seçebileceği yerdir. Önbelleğe almak arayan bunu hiç bilmeden. Bu nedenle bazen Arabulucu olarak anılabilir. Bazıları bunu Dekoratör deseninin başka bir biçimi olarak da sınıflandırabilir, ancak bazı farklılıklar vardır.
Bu yazıda JavaScript’teki Proxy Tasarım Modelinin gücünü gözden geçireceğiz ve bir sonraki uygulamanız için ne kadar faydalı olabileceğine dair birkaç örneği gözden geçireceğiz.
JavaScript yerel olarak bir Proxy
kalıbı uygulayan sınıf, doğrudan Proxy
birkaç Vanilya uygulamasından sonra kalıbı göstermek yerine sınıf.
Dekoratör ve Proxy arasındaki fark
Dekoratör deseninde, dekoratörün ana sorumluluğu sardığı (veya “dekorasyon yaptığı”) nesneyi geliştirmektir, oysa proxy daha fazla erişilebilirliğe sahiptir ve nesneyi kontrol eder.
Proxy, sardığı nesneyi geliştirmeyi veya dış dünyadan erişimi kısıtlamak gibi başka yollarla kontrol etmeyi seçebilir, ancak bunun yerine bir dekoratör geliştirmeleri bilgilendirir ve uygular.
Sorumluluk açısından fark açıktır. Mühendisler genellikle dekoratörleri yeni davranış eklemek için veya gelişmiş bir arabirim döndürdükleri eski veya eski sınıflar için bir bağdaştırıcı biçimi olarak kullanır. müşterinin bildiği ama aynı zamanda umursamadığı. Proxy’nin genellikle geri dönmesi amaçlanır istemcinin aynı nesneyle dokunulmadan çalıştığını varsayabileceği aynı arabirim.
Doğrulayıcı/Yardımcı
Burada göstereceğim bir Proxy modelinin ilk uygulaması bir doğrulayıcı olacaktır.
Bu örnek, girdiyi doğrulamaya ve özelliklerin yanlış veri türlerine ayarlanmasını önlemeye yardımcı olmak için uygulanan kalıbı gösterir. Arayanın her zaman orijinal nesneyle çalıştığını varsayması gerektiğini unutmayın, bu nedenle Proxy, sardığı nesnenin imzasını veya arabirimini değiştirmemelidir:
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'
Bu örnek, bir nesnenin alanlarını doğrulayan basit bir yardımcıyı gösterir. TypeError
doğrulama başarısız olduğunda istisna.
Proxy’nin sahipliğini alır getter
ve setter
arasında id
özellik ve ayarlanmaya çalışılan değerlere izin vermeyi veya reddetmeyi seçer.
İçinde Proxy
sınıf, bunun gibi bir şeyle uygulanabilir:
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'
Doğrulayıcı mükemmel çalışıyor:
TypeError: The id 3 is not a number. Received string instead
Pano Çoklu Dolgu
Bu bölüm, metin seçimlerini kullanıcı panosuna kopyalarken, tarayıcının aşağıdakileri desteklemesini sağlayarak eski tarayıcıları desteklemenin bir yolu olarak Proxy’yi kullanmaya devam edecektir. Navigator.clipboard
API. Olmazsa, kullanıma geri dönecektir. execCommand
Seçimi kopyalamak için
Yine, istemci her zaman metotları çağırdığı nesnenin orijinal nesne olduğunu varsayar ve yalnızca adı geçen yöntemi çağırdığını bilir:
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()
Uygulamayı doğrudan gerçek içine kodlamak yerine, bu gibi durumlarda proxy’yi uygulamanın amacının ne olduğunu sorabilirsiniz. copyToClipboard
işlev. Bir proxy kullanırsak, onu bağımsız olarak yeniden kullanabilir ve uygulamayı şu şekilde serbestçe değiştirebiliriz: kontrolün tersine çevrilmesi.
Bu stratejiyi kullanmanın bir başka yararı da orijinal işlevi değiştirmememizdir.
Önbellek (Performansı artırma)
Önbelleğe alma, birçok farklı senaryoda birçok farklı biçimde olabilir. Örneğin, bir Yeniden Doğrulanırken Bayat http istekleri için, nginx içerik önbelleğe alma, işlemci önbelleğe alma, tembel yükleme önbelleğe alma, not alma. vb.
JavaScript’te bir Proxy yardımıyla önbelleğe almayı da başarabiliriz.
Proxy modelini doğrudan kullanmadan uygulamak için Proxy
sınıf böyle bir şey yapabiliriz:
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')
önbellek:
{
"109105107101": "[hello] mike",
"11597108108121": "[hello] sally",
"109105107101321161041013210310597110116": "[hello] mike the giant",
"115971081081213211610410132108105116116108101": "[hello] sally the little",
"108111114100321111023211610410132114105110103115": "[hello] lord of the rings"
}
Bunu doğrudan bir Proxy
sınıf düz ileri:
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]
},
})
}
bu Proxy
sınıf
Birkaç barebone Proxy model uygulamasında kalıcı bir model gördük ve doğrudan Proxy
sınıf. JavaScript doğrudan sağladığından Proxy
dile bir nesne olarak, bu yazının geri kalanı bunu bir kolaylık olarak kullanacak.
Geri kalan tüm örnekler, Proxy
ancak bunun yerine sınıf sözdizimine odaklanacağız çünkü özellikle bu yazı uğruna çalışmak daha özlü ve daha kolay.
Singleton’a proxy
Bir Singleton’ı hiç duymadıysanız, bir uygulamanın ömrü boyunca zaten somutlaştırılmışsa, ilgilenilen bir nesnenin döndürülmesini ve yeniden kullanılmasını sağlayan başka bir tasarım modelidir. Pratikte büyük olasılıkla bunun bir global değişken olarak kullanıldığını göreceksiniz.
Örneğin, bir MMORPG oyunu kodluyor olsaydık ve üç sınıfımız olsaydı Equipment
, Person
ve Warrior
sadece nerede olabilir bir Warrior
varlığında, kullanabiliriz construct
bir örneği başlatırken ikinci argümanın içindeki işleyici yöntemi Proxy
üzerinde Warrior
sınıf:
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)
Birden çok örneğini oluşturmaya çalışırsak Warrior
her seferinde yalnızca ilk oluşturulanın kullanılması sağlanır:
const mike = new WarriorSingleton('mike')
const bob = new WarriorSingleton('bob')
const sally = new WarriorSingleton('sally')
console.log(mike)
console.log(bob)
console.log(sally)
Sonuç:
Warrior {
hp: 100,
name: 'mike',
equipments: { defense: {}, offense: {} }
}
Warrior {
hp: 100,
name: 'mike',
equipments: { defense: {}, offense: {} }
}
Warrior {
hp: 100,
name: 'mike',
equipments: { defense: {}, offense: {} }
}
çerez hırsızı
Bu bölümde, bir kullanarak bir örnek göstereceğiz Proxy
çerez listesinden mutasyonları önlemek için. Bu, orijinal nesnenin mutasyona uğramasını ve mutatörün ( CookieStealer
) kötü operasyonlarının başarılı olduğunu varsayacaktır.
Şu örneğe bir göz atalım:
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)
George’un yemeği:
{
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' }
]
}
somutlaştırdık george
kullanmak Human
sınıfı ve deposuna 7 adet yiyecek ekledik. George meyvelerini ve kurabiyelerini yemek üzere olduğu için mutludur. Kurabiyeleri konusunda özellikle heyecanlı çünkü en sevdiği lezzetlerin hepsini aynı anda alıyor ve kısa süre sonra kurabiye ihtiyacını gidermek için onları yemeye başlıyor.
Ancak bir sorun var:
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)
bu CookieStealer
kurabiyelerini çalmak için birdenbire ortaya çıkar. bu CookieStealer
şimdi deposunda 5 çerez var:
[
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 {
foods: [
Food { name: 'apple', points: 2 },
Food { name: 'banana', points: 2 }
]
}
Geriye sarılıp kurtarıcımızı tanıtacak olsaydık Superman
uygulayan yöntemlerinden birini uygulamak Proxy
önlemek için desen CookieStealer
onun kötü eylemlerinden sorunumuzu çözecekti:
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')
Bizim arkadaşımız superman
şans eseri olur protectFromCookieStealers
gücünü kullanarak Proxy
ile numara yapmak çerez listesi! o tutar gerçek George’un kurabiyelerinden saklanan yiyeceklerin koleksiyonu. CookieStealer
. CookieStealer
şeytani planlarıyla ilerliyor ve görünüşe göre kandırılmış kurabiyelerden kurtulduğunu düşünerek:
CookieStealer.stealCookies(slickGeorge)
console.log(CookieStealer.cookies)
bu CookieStealer
hala deposunda kurabiyelerle uzaklaşıyor ve düşünüyor ondan uzaklaştı:
[
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' }
]
Süpermen tarafından kandırıldığını çok az biliyor ve bunlar sahte kurabiyeler! george
gücü sayesinde hala kurabiyelerine dokunulmamış durumda. Proxy
onu kötülüğün karanlığından kurtarmak:
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' }
]
}
Çözüm
Umarım bu, Proxy modeline ve şimdi yerleşik olarak bu konseptten nasıl yararlanılacağına biraz ışık tutmaya yardımcı olmuştur. Proxy
JavaScript’te sınıf.
Bu yazının sonu böyle bitiyor 🙂 Umarım bu makaleyi sizin için yararlı bulmuşsunuzdur ve gelecekteki gönderiler için beni medyadan takip ettiğinizden emin olun!