Objekt třídy Proxy obaluje jiný objekt a zachytává operace na něm, například čtení nebo zápis vlastností a jiné. Volitelně je zpracovává sám o sobě nebo průhledně umožňuje objektu, aby je zpracovával sám.
Proxy jsou používány v mnoha knihovnách a některých frameworcích prohlížečů. V tomto článku uvidíme mnoho praktických aplikací.
Proxy
Syntaxe:
let proxy = new Proxy(cíl, handler)
cíl– je objekt, který má být obalen, může to být cokoli včetně funkcí.handler– konfigurace proxy: objekt s „pastmi“, metodami, které zachytávají operace, např. pastgetpro načítání vlastností objektucíl, pastsetpro zápis vlastnosti do objektucíl, a tak dále.
Jestliže pro operaci prováděnou na proxy existuje odpovídající past v objektu handler, pak se spustí a proxy dostane šanci operaci zpracovat, v opačném případě je operace provedena na objektu cíl.
Jako počáteční příklad vytvořme proxy bez jakýchkoli pastí:
let cíl = {};
let proxy = new Proxy(cíl, {}); // prázdný handler
proxy.test = 5; // zápis do proxy (1)
alert(cíl.test); // 5, vlastnost se objeví v objektu cíl!
alert(proxy.test); // 5, můžeme ji z proxy také načíst (2)
for(let klíč in proxy) alert(klíč); // test, iterace funguje (3)
Protože zde nejsou žádné pasti, všechny operace na proxy jsou předány objektu cíl.
- Operace zápisu
proxy.test=nastaví hodnotu v objektucíl. - Operace čtení
proxy.testvrátí hodnotu z objektucíl. - Iterace nad objektem
proxyvrací hodnoty z objektucíl.
Jak vidíme, bez pastí je proxy průhledným obalem kolem objektu cíl.
Proxy je speciální „exotický objekt“, který nemá své vlastní vlastnosti. Je-li objekt handler prázdný, průhledně předává operace objektu cíl.
Abychom aktivovali jeho další schopnosti, přidejme pasti.
Co s nimi můžeme zachytávat?
Pro většinu operací na objektech je ve specifikaci JavaScriptu tzv. „interní metoda“, která popisuje, jak operace funguje na nejnižší úrovni. Například [[Get]], interní metoda k načtení vlastnosti, [[Set]], interní metoda k zápisu vlastnosti, a tak dále. Tyto metody se používají jen ve specifikaci, volat přímo názvem je nemůžeme.
Pasti proxy zachytávají vyvolávání těchto metod. Jsou vyjmenovány ve specifikaci Proxy a v následující tabulce.
Pro každou interní metodu je v této tabulce past: název metody, který můžeme přidat do parametru handler při volání new Proxy, abychom operaci zachytili:
| Interní metoda | Metoda handleru | Spustí se při… |
|---|---|---|
[[Get]] |
get |
načítání vlastnosti |
[[Set]] |
set |
zápisu do vlastnosti |
[[HasProperty]] |
has |
operátoru in |
[[Delete]] |
deleteProperty |
operátoru delete |
[[Call]] |
apply |
volání funkce |
[[Construct]] |
construct |
operátoru new |
[[GetPrototypeOf]] |
getPrototypeOf |
Object.getPrototypeOf |
[[SetPrototypeOf]] |
setPrototypeOf |
Object.setPrototypeOf |
[[IsExtensible]] |
isExtensible |
Object.isExtensible |
[[PreventExtensions]] |
preventExtensions |
Object.preventExtensions |
[[DefineOwnProperty]] |
defineProperty |
Object.defineProperty, Object.defineProperties |
[[GetOwnProperty]] |
getOwnPropertyDescriptor |
Object.getOwnPropertyDescriptor, for..in, Object.keys/values/entries |
[[OwnPropertyKeys]] |
ownKeys |
Object.getOwnPropertyNames, Object.getOwnPropertySymbols, for..in, Object.keys/values/entries |
JavaScript vyžaduje některé invarianty – podmínky, které musejí interní metody a pasti splňovat.
Většina z nich se týká návratových hodnot:
[[Set]]musí vracettrue, jestliže byla hodnota úspěšně zapsána, jinakfalse.[[Delete]]musí vracettrue, jestliže byla hodnota úspěšně smazána, jinakfalse.- …a tak dále, další uvidíme v příkladech níže.
Existují i některé další invarianty, například:
[[GetPrototypeOf]]aplikovaná na proxy objekt musí vracet stejnou hodnotu jako[[GetPrototypeOf]]aplikovaná na cílový objekt tohoto proxy objektu. Jinými slovy, načítání prototypu proxy musí vždy vrátit prototyp cílového objektu.
Pasti mohou tyto operace zachytávat, ale musejí dodržovat tato pravidla.
Invarianty zajišťují korektní a konzistentní chování prvků jazyka. Úplný seznam invariant je obsažen ve specifikaci. Pokud nebudete dělat něco podivného, pravděpodobně je neporušíte.
Podívejme se na praktických příkladech, jak to funguje.
Výchozí hodnota s pastí „get“
Nejběžnější pasti jsou pro čtení a zápis vlastností.
Abychom zachytili čtení, handler by měl obsahovat metodu get(cíl, vlastnost, příjemce).
Když je vlastnost načítána, tato metoda se spustí s následujícími argumenty:
cíl– je cílový objekt, ten, který byl předán donew Proxyjako první argument,vlastnost– název vlastnosti,příjemce– je-li cílová vlastnost getter, pakpříjemceje objekt, který bude při volání tohoto getteru použit jakothis. Obvykle je to samotný objektproxy(nebo objekt, který je z něj zděděn, pokud dědíme z proxy). Prozatím tento argument nepotřebujeme, takže ho podrobněji vysvětlíme později.
Použijme get k implementaci výchozích hodnot objektu.
Vytvořme číselné pole, které bude pro neexistující hodnoty vracet 0.
Když se pokoušíme načíst neexistující prvek pole, obyčejně získáme undefined, ale my obalíme běžné pole do proxy, která bude obsahovat past na načítání a vracet 0, pokud načítaná vlastnost neexistuje:
let čísla = [0, 1, 2];
čísla = new Proxy(čísla, {
get(cíl, vlastnost) {
if (vlastnost in cíl) {
return cíl[vlastnost];
} else {
return 0; // výchozí hodnota
}
}
});
alert( čísla[1] ); // 1
alert( čísla[123] ); // 0 (takový prvek není)
Jak vidíme, s pastí get je něco takového docela snadné.
Můžeme použít Proxy k implementaci jakékoli logiky pro „výchozí“ hodnoty.
Představme si, že máme slovník s větami a jejich překlady:
let slovník = {
'Ahoj': 'Hola',
'Nashle': 'Adiós'
};
alert( slovník['Ahoj'] ); // Hola
alert( slovník['Vítejte'] ); // undefined
Prozatím načtení věty, kterou slovník neobsahuje, vrací undefined. V praxi je však obvykle lepší nechat větu nepřeloženou než vrátit undefined. Nechme tedy slovník, aby v takovém případě vrátil místo undefined nepřeloženou větu.
Abychom toho dosáhli, obalíme slovník do proxy, která zachytává operace načítání:
let slovník = {
'Ahoj': 'Hola',
'Nashle': 'Adiós'
};
slovník = new Proxy(slovník, {
get(cíl, věta) { // zachytíme načítání vlastnosti ze slovníku
if (věta in cíl) { // máme-li ji ve slovníku,
return cíl[věta]; // vrátíme její překlad
} else {
// jinak vrátíme nepřeloženou větu
return věta;
}
}
});
// Podívejte se na různé věty ve slovníku!
// Přinejhorším nebudou přeloženy.
alert( slovník['Ahoj'] ); // Hola
alert( slovník['Vítejte v Proxy']); // Vítejte v Proxy (bez překladu)
Prosíme všimněte si, jak proxy přepisuje proměnnou:
slovník = new Proxy(slovník, ...);
Proxovaný objekt by měl cílový objekt všude zcela nahradit. Na cílový objekt by se po jeho nahrazení proxovaným objektem neměl nikdo nikde odkazovat. Jinak snadno naděláme nepořádek.
Ověřování pomocí pasti „set“
Řekněme, že chceme pole výlučně pro čísla. Bude-li přidána hodnota jiného typu, měla by nastat chyba.
Když se zapisuje do vlastnosti, spustí se past set.
set(cíl, vlastnost, hodnota, příjemce):
cíl– je cílový objekt, ten, který byl předán donew Proxyjako první argument,vlastnost– název vlastnosti,hodnota– hodnota vlastnosti,příjemce– podobně jako u pastiget, má význam jen pro settery.
Past set by měla vracet true, je-li nastavení úspěšné, a jinak false (vyvolá TypeError).
Použijme ji k ověřování nových hodnot:
let čísla = [];
čísla = new Proxy(čísla, { // (*)
set(cíl, vlastnost, hodnota) { // zachytává zápis do vlastností
if (typeof hodnota == 'number') {
cíl[vlastnost] = hodnota;
return true;
} else {
return false;
}
}
});
čísla.push(1); // úspěšně přidáno
čísla.push(2); // úspěšně přidáno
alert("Délka pole je: " + čísla.length); // 2
čísla.push("test"); // TypeError ('set' na proxy vrátila false)
alert("Na tento řádek se nikdy nedostaneme (chyba na řádku výše)");
Prosíme všimněte si, že zabudovaná funkcionalita polí stále funguje! Hodnoty se přidávají metodou push. Vlastnost length se při přidávání hodnot automaticky zvyšuje. Naše proxy nic nepokazila.
Metody přidávání hodnot do polí jako push, unshift a podobně nemusíme kvůli přidání ověření přepisovat, protože vnitřně používají operaci [[Set]], kterou proxy zachytává.
Kód je tedy čistý a výstižný.
trueJak bylo řečeno výše, existují invarianty, které je třeba dodržovat.
Pro set musíme při úspěšném zápisu vracet true.
Pokud na to zapomeneme nebo vrátíme jakoukoli nepravdivou hodnotu, operace vyvolá TypeError.
Iterace pomocí „ownKeys“ a „getOwnPropertyDescriptor“
Metoda Object.keys, cyklus for..in a většina ostatních metod, které iterují nad vlastnostmi objektu, používají k načtení seznamu vlastností interní metodu [[OwnPropertyKeys]] (kterou zachytává past ownKeys).
Tyto metody se liší v detailech:
Object.getOwnPropertyNames(obj)vrací nesymbolické klíče.Object.getOwnPropertySymbols(obj)vrací symbolické klíče.Object.keys/values()vrací nesymbolické klíče/hodnoty s přepínačemenumerable(přepínače vlastností byly vysvětleny v kapitole Přepínače a deskriptory vlastností).for..incykluje nad nesymbolickými klíči s přepínačemenumerablea také nad klíči prototypu.
…Všechny však začínají se seznamem vlastností vráceným touto metodou.
V následujícím příkladu použijeme past ownKeys, abychom cyklus for..in nad objektem uživatel, stejně jako metody Object.keys a Object.values, přiměli přeskakovat vlastnosti začínající podtržítkem _:
let uživatel = {
jméno: "Jan",
věk: 30,
_heslo: "***"
};
uživatel = new Proxy(uživatel, {
ownKeys(cíl) {
return Object.keys(cíl).filter(klíč => !klíč.startsWith('_'));
}
});
// „ownKeys“ odfiltruje _heslo
for(let klíč in uživatel) alert(klíč); // jméno, pak: věk
// stejný efekt má na těchto metodách:
alert( Object.keys(uživatel) ); // jméno,věk
alert( Object.values(uživatel) ); // Jan,30
Dosud to funguje.
Ale jestliže vrátíme klíč, který v tomto objektu neexistuje, Object.keys jej nezpracuje:
let uživatel = { };
uživatel = new Proxy(uživatel, {
ownKeys(cíl) {
return ['a', 'b', 'c'];
}
});
alert( Object.keys(uživatel) ); // <prázdný seznam>
Proč? Důvod je prostý: metoda Object.keys vrací jen vlastnosti s přepínačem enumerable. Aby si jej ověřila, volá pro každou vlastnost interní metodu [[GetOwnProperty]], aby získala její deskriptor. A protože zde žádná vlastnost není, její deskriptor je prázdný a neobsahuje přepínač enumerable, a tak je vlastnost přeskočena.
Aby Object.keys vracel vlastnost, musí buď tato vlastnost v objektu existovat s přepínačem enumerable, nebo můžeme zachytávat volání metody [[GetOwnProperty]] (to dělá past getOwnPropertyDescriptor) a vracet deskriptor obsahující enumerable: true.
Zde je příklad:
let uživatel = { };
uživatel = new Proxy(uživatel, {
ownKeys(cíl) { // volána jednou pro získání seznamu vlastností
return ['a', 'b', 'c'];
},
getOwnPropertyDescriptor(cíl, vlastnost) { // volána pro každou vlastnost
return {
enumerable: true,
configurable: true
/* ...další přepínače, pravděpodobně "value:..." */
};
}
});
alert( Object.keys(uživatel) ); // a, b, c
Poznamenejme to znovu: jestliže vlastnost v objektu chybí, potřebujeme jen zachycovat [[GetOwnProperty]].
Ochrana vlastností pomocí „deleteProperty“ a jiných pastí
Existuje široce přijímaná konvence, že vlastnosti a metody začínající podtržítkem _ jsou interní. Nemělo by se k nim přistupovat zvnějšku objektu.
Technicky to však možné je:
let uživatel = {
jméno: "Jan",
_heslo: "tajné"
};
alert(uživatel._heslo); // tajné
Abychom zabránili jakémukoli přístupu k vlastnostem, které začínají _, použijme proxy.
Potřebujeme tyto pasti:
getk vyvolání chyby při načítání takové vlastnosti,setk vyvolání chyby při zapisování do ní,deletePropertyk vyvolání chyby při jejím mazání,ownKeysk vyloučení vlastností začínajících_z cyklufor..ina metod jakoObject.keys.
Zde je kód:
let uživatel = {
jméno: "Jan",
_heslo: "***"
};
uživatel = new Proxy(uživatel, {
get(cíl, vlastnost) {
if (vlastnost.startsWith('_')) {
throw new Error("Přístup zamítnut");
}
let hodnota = cíl[vlastnost];
return (typeof hodnota === 'function') ? hodnota.bind(cíl) : hodnota; // (*)
},
set(cíl, vlastnost, hodnota) { // k zachycení zápisu do vlastnosti
if (vlastnost.startsWith('_')) {
throw new Error("Přístup zamítnut");
} else {
cíl[vlastnost] = hodnota;
return true;
}
},
deleteProperty(cíl, vlastnost) { // k zachycení mazání vlastnosti
if (vlastnost.startsWith('_')) {
throw new Error("Přístup zamítnut");
} else {
delete cíl[vlastnost];
return true;
}
},
ownKeys(cíl) { // k zachycení seznamu vlastností
return Object.keys(cíl).filter(klíč => !klíč.startsWith('_'));
}
});
// „get“ neumožňuje načíst _heslo
try {
alert(uživatel._heslo); // Chyba: Přístup zamítnut
} catch(e) { alert(e.message); }
// „set“ neumožňuje zapsat do _heslo
try {
uživatel._heslo = "test"; // Chyba: Přístup zamítnut
} catch(e) { alert(e.message); }
// „deleteProperty“ neumožňuje smazat _heslo
try {
delete uživatel._heslo; // Chyba: Přístup zamítnut
} catch(e) { alert(e.message); }
// „ownKeys“ odfiltruje _heslo
for(let klíč in uživatel) alert(klíč); // jméno
Prosíme všimněte si důležitého detailu v pasti get na řádku (*):
get(cíl, vlastnost) {
// ...
let hodnota = cíl[vlastnost];
return (typeof hodnota === 'function') ? hodnota.bind(cíl) : hodnota; // (*)
}
Proč potřebujeme, aby funkce volala hodnota.bind(cíl)?
Důvodem je, že objektové metody, např. uživatel.ověřHeslo(), musejí být schopny k _heslo přistupovat:
uživatel = {
// ...
ověřHeslo(hodnota) {
// objektová metoda musí být schopna _heslo číst
return hodnota === this._heslo;
}
}
Volání uživatel.ověřHeslo() získá jako this proxovaný objekt uživatel (objekt před tečkou se stane this), takže když se pokusí o přístup k this._heslo, aktivuje se past get (ta se spustí při načítání jakékoli vlastnosti) a vyvolá se chyba.
Proto na řádku (*) navážeme kontext objektových metod na původní objekt, cíl. Pak jejich následná volání budou jako this používat cíl bez jakýchkoli pastí.
Toto řešení zpravidla funguje, ale není ideální, protože nějaká metoda může předat neproxovaný objekt někam jinam a pak nastane zmatek: kde je původní objekt a kde proxovaný?
Kromě toho objekt může být proxován několikrát (vícenásobné proxy mohou k objektu přidávat různé „úpravy“), a jestliže do metody předáme neobalený objekt, důsledky mohou být nečekané.
Taková proxy by tedy neměla být používána všude.
Moderní JavaScriptové motory nativně podporují ve třídách soukromé vlastnosti, začínající znakem #. Jsou popsány v článku Soukromé a chráněné vlastnosti a metody. Žádné proxy nejsou zapotřebí.
Takové vlastnosti však mají své vlastní problémy. Například nejsou děděny.
„in“ pro rozsah s pastí „has“
Podívejme se na další příklady.
Máme objekt rozsahu:
let rozsah = {
začátek: 1,
konec: 10
};
K ověření, zda číslo leží v rozsahu rozsah, bychom rádi používali operátor in.
Volání in zachytává past has.
has(cíl, vlastnost)
cíl– je cílový objekt, předaný jako první argument donew Proxy,vlastnost– název vlastnosti.
Zde je demo:
let rozsah = {
začátek: 1,
konec: 10
};
rozsah = new Proxy(rozsah, {
has(cíl, vlastnost) {
return vlastnost >= cíl.začátek && vlastnost <= cíl.konec;
}
});
alert(5 in rozsah); // true
alert(50 in rozsah); // false
Pěkný syntaktický cukr, že? A implementuje se velmi jednoduše.
Obalování funkcí: „apply“
Do proxy můžeme obalit i funkci.
Past apply(cíl, thisArg, args) zpracovává volání proxy jako funkce:
cílje cílový objekt (funkce je v JavaScriptu objekt),thisArgje hodnotathis,argsje seznam argumentů.
Vzpomeňme si například na dekorátor zpozdi(f, ms), který jsme vytvořili v článku Dekorátory a přesměrování, call/apply.
V onom článku jsme to udělali bez proxy. Volání zpozdi(f, ms) vrátilo funkci, která funkci f předává všechna volání za ms milisekund.
Zde je předchozí implementace založená na funkcích:
function zpozdi(f, ms) {
// vrátí obal, který po uplynutí zadaného času předá volání funkci f
return function() { // (*)
setTimeout(() => f.apply(this, arguments), ms);
};
}
function řekniAhoj(uživatel) {
alert(`Ahoj, ${uživatel}!`);
}
// po tomto obalení budou volání řekniAhoj pozdržena o 3 sekundy
řekniAhoj = zpozdi(řekniAhoj, 3000);
řekniAhoj("Jan"); // Ahoj, Jan! (po 3 sekundách)
Jak jsme již viděli, většinou to funguje. Obalová funkce (*) provede volání po stanoveném čase.
Avšak obalová funkce nepředává dál operace čtení a zápisu do vlastností nebo cokoli jiného. Po obalení ztratíme přístup k vlastnostem původní funkce, např. name, length a jiným:
function zpozdi(f, ms) {
return function() {
setTimeout(() => f.apply(this, arguments), ms);
};
}
function řekniAhoj(uživatel) {
alert(`Ahoj, ${uživatel}!`);
}
alert(řekniAhoj.length); // 1 (length=délka, délka funkce je počet argumentů v její deklaraci)
řekniAhoj = zpozdi(řekniAhoj, 3000);
alert(řekniAhoj.length); // 0 (v deklaraci obalu je 0 argumentů)
Proxy je mnohem silnější, jelikož cílovému objektu předává všechno.
Použijme Proxy místo obalové funkce:
function zpozdi(f, ms) {
return new Proxy(f, {
apply(cíl, thisArg, args) {
setTimeout(() => cíl.apply(thisArg, args), ms);
}
});
}
function řekniAhoj(uživatel) {
alert(`Ahoj, ${uživatel}!`);
}
řekniAhoj = zpozdi(řekniAhoj, 3000);
alert(řekniAhoj.length); // 1 (*) proxy předá cíli operaci „get length“
řekniAhoj("Jan"); // Ahoj, Jan! (po 3 sekundách)
Výsledek je stejný, ale nyní se původní funkci předávají nejen volání, ale všechny operace na proxy. Po obalení se tedy řekniAhoj.length na řádku (*) vrátí správně.
Získali jsme „bohatší“ obal.
Existují i jiné pasti: jejich úplný seznam je uveden na začátku tohoto článku. Jejich vzorec použití je podobný uvedenému.
Reflect
Reflect je vestavěný objekt, který zjednodušuje vytváření Proxy.
Již jsme uvedli, že interní metody, např. [[Get]], [[Set]] a jiné, jsou jen specifikační a nemůžeme je volat přímo.
Objekt Reflect to částečně umožňuje. Jeho metody jsou minimální obaly okolo interních metod.
Zde jsou příklady operací a volání Reflect, která udělají totéž:
| Operace | Volání Reflect |
Interní metoda |
|---|---|---|
obj[vlastnost] |
Reflect.get(obj, vlastnost) |
[[Get]] |
obj[vlastnost] = hodnota |
Reflect.set(obj, vlastnost, hodnota) |
[[Set]] |
delete obj[vlastnost] |
Reflect.deleteProperty(obj, vlastnost) |
[[Delete]] |
new F(hodnota) |
Reflect.construct(F, hodnota) |
[[Construct]] |
| … | … | … |
Například:
let uživatel = {};
Reflect.set(uživatel, 'jméno', 'Jan');
alert(uživatel.jméno); // Jan
Konkrétně nám Reflect umožňuje volat operátory (new, delete…) jako funkce (Reflect.construct, Reflect.deleteProperty, …). To je zajímavá schopnost, ale tady je důležitá jiná věc.
Pro každou interní metodu, kterou může zachytit Proxy, je v Reflect odpovídající metoda se stejným názvem a argumenty jako past v Proxy.
Můžeme tedy používat Reflect k předání operace původnímu objektu.
V tomto příkladu obě pasti get a set průhledně (jako by neexistovaly) předají objektu operace čtení nebo zápisu a zobrazí zprávu:
let uživatel = {
jméno: "Jan",
};
uživatel = new Proxy(uživatel, {
get(cíl, vlastnost, příjemce) {
alert(`GET ${vlastnost}`);
return Reflect.get(cíl, vlastnost, příjemce); // (1)
},
set(cíl, vlastnost, hodnota, příjemce) {
alert(`SET ${vlastnost}=${hodnota}`);
return Reflect.set(cíl, vlastnost, hodnota, příjemce); // (2)
}
});
let jméno = uživatel.jméno; // zobrazí "GET jméno"
uživatel.jméno = "Petr"; // zobrazí "SET jméno=Petr"
Zde:
Reflect.getnačte vlastnost objektu.Reflect.setzapíše vlastnost objektu a vrátítrue, je-li úspěšná, jinakfalse.
Všechno je přitom jednoduché: jestliže past chce předat volání objektu, stačí jí volat Reflect.<metoda> se stejnými argumenty.
Ve většině případů můžeme udělat totéž i bez Reflect, například načítání vlastnosti pomocí Reflect.get(cíl, vlastnost, příjemce) můžeme nahradit za cíl[vlastnost]. Jsou tady však důležité drobnosti.
Proxování getteru
Podívejme se na příklad, který demonstruje, proč je Reflect.get lepší. A také uvidíme, proč get/set mají třetí argument příjemce, který jsme zatím nepoužívali.
Máme objekt uživatel s vlastností _jméno a jejím getterem.
Zde je proxy okolo něj:
let uživatel = {
_jméno: "Host",
get jméno() {
return this._jméno;
}
};
let uživatelProxy = new Proxy(uživatel, {
get(cíl, vlastnost, příjemce) {
return cíl[vlastnost];
}
});
alert(uživatelProxy.jméno); // Host
Past get je zde „průhledná“, vrací původní vlastnost a nic jiného nedělá. To pro náš příklad stačí.
Vypadá to, že je všechno v pořádku. Učiňme však tento příklad trochu složitějším.
Po zdědění jiného objektu správce z objektu uživatel můžeme pozorovat nesprávné chování:
let uživatel = {
_jméno: "Host",
get jméno() {
return this._jméno;
}
};
let uživatelProxy = new Proxy(uživatel, {
get(cíl, vlastnost, příjemce) {
return cíl[vlastnost]; // (*) cíl = uživatel
}
});
let správce = {
__proto__: uživatelProxy,
_jméno: "Správce"
};
// Očekáváme: Správce
alert(správce.jméno); // vypíše: Host (?!?)
Načtení správce.jméno by mělo vrátit "Správce", ne "Host"!
V čem je problém? Udělali jsme snad něco špatně s dědičností?
Pokud však odstraníme proxy, bude všechno fungovat tak, jak očekáváme.
Problém je ve skutečnosti v proxy na řádku (*).
-
Když načítáme
správce.jméno, objektsprávcetakovou vlastnost nemá, a proto hledání přejde k jeho prototypu. -
Prototypem je
uživatelProxy. -
Když načítáme vlastnost
jménoz proxy, spustí se jeho pastgeta na řádku(*)ji vrátí z původního objektu jakocíl[vlastnost].Volání
cíl[vlastnost], kdyžvlastnostje getter, spustí kód tohoto getteru v kontextuthis=cíl. Výsledkem je tedythis._jménoz původního objektucíl, což je: z objektuuživatel.
Abychom takové situace opravili, potřebujeme příjemce, třetí argument pasti get. Ten udržuje správné this, které bude předáno getteru. V našem případě to je správce.
Jak předat kontext getteru? Pro běžnou funkci bychom mohli použít call/apply, ale tohle je getter, ten se „nevolá“, jenom se k němu přistupuje.
Může to udělat Reflect.get. Pokud ji použijeme, bude všechno fungovat správně.
Zde je opravená varianta:
let uživatel = {
_jméno: "Host",
get jméno() {
return this._jméno;
}
};
let uživatelProxy = new Proxy(uživatel, {
get(cíl, vlastnost, příjemce) { // příjemce = správce
return Reflect.get(cíl, vlastnost, příjemce); // (*)
}
});
let správce = {
__proto__: uživatelProxy,
_jméno: "Správce"
};
alert(správce.jméno); // Správce
Nyní je getteru předán příjemce, který si udržuje odkaz na správné this (což je správce), pomocí Reflect.get na řádku (*).
Tuto past můžeme ještě zkrátit:
get(cíl, vlastnost, příjemce) {
return Reflect.get(...arguments);
}
Metody Reflect jsou pojmenovány přesně stejně jako pasti a přijímají stejné argumenty. Byly tak úmyslně navrženy.
return Reflect... tedy poskytuje bezpečný a srozumitelný způsob, jak předat dál operaci a zajistit, abychom nezapomněli na nic, co se k ní vztahuje.
Omezení proxy
Proxy poskytují unikátní způsob, jak změnit nebo upravit chování existujících objektů na nejnižší úrovni. Přesto nejsou dokonalé a mají svá omezení.
Zabudované objekty: Interní sloty
Mnoho zabudovaných objektů, např. Map, Set, Date, Promise a jiné, využívá tzv. „interní sloty“.
Podobají se vlastnostem, ale jsou rezervovány pro vnitřní, výhradně specifikační účely. Například Map si ukládá prvky do interního slotu [[MapData]]. Vestavěné metody k nim přistupují přímo, ne interními metodami [[Get]]/[[Set]]. Proxy je tedy nemůže zachytit.
Proč se o to starat? Jsou přece interní!
Zde je důvod. Když bude podobný vestavěný objekt proxován, proxy objekt nebude tyto interní sloty mít, takže vestavěné metody selžou.
Například:
let mapa = new Map();
let proxy = new Proxy(mapa, {});
proxy.set('test', 1); // Chyba
Vnitřně si Map ukládá všechna data do svého interního slotu [[MapData]]. Proxy takový slot nemá. Vestavěná metoda Map.prototype.set se pokusí přistoupit k interní vlastnosti this.[[MapData]], ale protože this=proxy, nenajde ji v proxy a prostě selže.
Naštěstí existuje způsob, jak to opravit:
let mapa = new Map();
let proxy = new Proxy(mapa, {
get(cíl, vlastnost, příjemce) {
let hodnota = Reflect.get(...arguments);
return typeof hodnota == 'function' ? hodnota.bind(cíl) : hodnota;
}
});
proxy.set('test', 1);
alert(proxy.get('test')); // 1 (funguje!)
Teď to funguje dobře, protože past get naváže funkční vlastnosti, např. mapa.set, na samotný cílový objekt (mapa).
Na rozdíl od předchozího příkladu hodnota this uvnitř proxy.set(...) nebude proxy, ale původní mapa. Když se tedy vnitřní implementace metody set pokusí přistoupit k internímu slotu this.[[MapData]], uspěje.
Array nemá interní slotyVýznamná výjimka: vestavěné Array nepoužívá interní sloty. Je tomu tak z historických důvodů, jelikož se objevilo již před dlouhou dobou.
Při proxování pole tedy takový problém nenastává.
Soukromá pole
Obdobný problém nastává se soukromými třídními poli.
Například metoda vraťJméno() přistupuje k soukromé vlastnosti #jméno a po proxování se rozbije:
class Uživatel {
#jméno = "Host";
vraťJméno() {
return this.#jméno;
}
}
let uživatel = new Uživatel();
uživatel = new Proxy(uživatel, {});
alert(uživatel.vraťJméno()); // Chyba
Důvodem je, že soukromá pole jsou implementována pomocí interních slotů. Při přístupu k nim JavaScript nepoužívá [[Get]]/[[Set]].
Při volání vraťJméno() je hodnota this proxovaný uživatel, který neobsahuje slot se soukromými poli.
Opět to opraví řešení s navázáním metody:
class Uživatel {
#jméno = "Host";
vraťJméno() {
return this.#jméno;
}
}
let uživatel = new Uživatel();
uživatel = new Proxy(uživatel, {
get(cíl, vlastnost, příjemce) {
let hodnota = Reflect.get(...arguments);
return typeof hodnota == 'function' ? hodnota.bind(cíl) : hodnota;
}
});
alert(uživatel.vraťJméno()); // Host
Při tom všem však toto řešení má nevýhody, jak bylo vysvětleno dříve: vystaví metodě původní objekt, čímž umožní, aby byl předáván dál a rozbíjel ostatní proxovanou funkcionalitu.
Proxy != cíl
Proxovaný a původní objekt jsou různé objekty. To je přirozené, ne?
Když tedy použijeme původní objekt jako klíč a pak jej naproxujeme, proxovaný objekt nebude nalezen:
let všichniUživatelé = new Set();
class Uživatel {
constructor(jméno) {
this.jméno = jméno;
všichniUživatelé.add(this);
}
}
let uživatel = new Uživatel("Jan");
alert(všichniUživatelé.has(uživatel)); // true
uživatel = new Proxy(uživatel, {});
alert(všichniUživatelé.has(uživatel)); // false
Jak vidíme, po naproxování nenajdeme objekt uživatel v množině všichniUživatelé, jelikož proxovaný objekt je jiný.
===Proxy mohou zachytit mnoho operátorů, např. new (pomocí construct), in (pomocí has), delete (pomocí deleteProperty) a tak dále.
Neexistuje však žádný způsob, jak zachytit test striktní rovnosti objektů. Objekt je striktně roven pouze sám sobě a žádné jiné hodnotě.
Všechny operace a vestavěné třídy, které porovnávají objekty, tedy budou rozlišovat mezi objektem a jeho proxy. Tady neexistuje žádná průhledná náhrada.
Zrušitelné proxy
Zrušitelná proxy je proxy, která může být zakázána.
Řekněme, že máme zdroj a chtěli bychom k němu kdykoli uzavřít přístup.
Můžeme to udělat tak, že jej obalíme do zrušitelné proxy bez jakýchkoli pastí. Taková proxy pak bude předávat objektu operace a my ji budeme moci kdykoli zakázat.
Syntaxe je:
let {proxy, revoke} = Proxy.revocable(cíl, handler)
Toto volání vrátí objekt s proxy a funkci revoke, která tuto proxy zakáže.
Zde je příklad:
let objekt = {
data: "Cenná data"
};
let {proxy, revoke} = Proxy.revocable(objekt, {});
// předáme někam proxy místo objektu...
alert(proxy.data); // Cenná data
// později v našem kódu
revoke();
// proxy nadále nefunguje (je zakázána)
alert(proxy.data); // Chyba
Volání revoke() odstraní z proxy veškeré interní odkazy na cílový objekt, takže již nebudou propojeny.
Na začátku je revoke od proxy odděleno, takže můžeme předávat proxy dále, zatímco revoke zůstane v aktuálním rozsahu platnosti.
Můžeme také metodu revoke navázat na proxy nastavením proxy.revoke = revoke.
Další možností je vytvořit WeakMap, která obsahuje proxy jako klíč a odpovídající metodu revoke jako hodnotu, což nám umožní snadno najít revoke pro požadovanou proxy:
let zákazy = new WeakMap();
let objekt = {
data: "Cenná data"
};
let {proxy, revoke} = Proxy.revocable(objekt, {});
zákazy.set(proxy, revoke);
// ..někde jinde v našem kódu..
revoke = zákazy.get(proxy);
revoke();
alert(proxy.data); // Chyba (zakázáno)
Zde používáme WeakMap místo Map, protože neblokuje sběr odpadků. Pokud se proxovaný objekt stane „nedosažitelným“ (např. protože na něj už nebude odkazovat žádná proměnná), WeakMap umožní, aby byl odstraněn z paměti spolu s jeho metodou revoke, která už nadále nebude zapotřebí.
Odkazy
Shrnutí
Proxy je obal okolo objektu, který objektu předává operace na něm prováděné a může některé z nich zachytit.
Může obalit objekt jakéhokoli druhu včetně tříd a funkcí.
Syntaxe je:
let proxy = new Proxy(cíl, {
/* pasti */
});
…Pak můžeme všude používat proxy místo objektu cíl. Proxy nemá své vlastní vlastnosti nebo metody. Jestliže mu je poskytnuta past, zachytí příslušnou operaci, jinak ji předá objektu cíl.
Můžeme zachytávat:
- Čtení (
get), zápis (set), mazání (deleteProperty) vlastnosti (i neexistující). - Volání funkce (past
apply). - Operátor
new(pastconstruct). - Mnoho dalších operací (jejich úplný seznam je na začátku tohoto článku a v dokumentaci).
To nám umožňuje vytvářet „virtuální“ vlastnosti a metody, implementovat výchozí hodnoty, pozorovatelné objekty, dekorátory funkcí a mnoho dalšího.
Můžeme také obalit objekt vícekrát do různých proxy a dekorovat jej tak různými aspekty funkcionality.
API Reflect je navrženo k doplnění Proxy. Pro každou past Proxy existuje v Reflect odpovídající metoda se stejnými argumenty. K předávání volání cílovým objektům bychom měli používat tyto metody.
Proxy mají určitá omezení:
- Vestavěné objekty mají „interní sloty“ a přístup k nim nemůže být proxován. Viz výše.
- Totéž platí pro soukromá třídní pole, protože ta jsou vnitřně implementována pomocí slotů. Volání proxovaných metod tedy musí nastavovat cílový objekt jako
this, aby se k nim dalo přistupovat. - Nelze zachytávat testy rovnosti objektů
===. - Výkon: výsledky benchmarků závisejí na motoru, ale obecně přístup k vlastnosti i přes tu nejjednodušší proxy trvá několikrát déle. V praxi na tom však záleží jen u některých objektů, které tvoří „úzké hrdlo“.
Komentáře
<code>, pro několik řádků je obalte značkou<pre>, pro více než 10 řádků vložte odkaz na pískoviště (plnkr, jsbin, codepen…)