A Proxy
objekt zalamuje jiný objekt a zachycuje operace, jako je čtení/zápis vlastností a další, volitelně je zpracovává samostatně nebo transparentně umožňuje, aby je zpracovával objekt.
Proxy se používají v mnoha knihovnách a některých rámcích prohlížečů. V tomto článku uvidíme mnoho praktických aplikací.
Proxy
Syntaxe:
let proxy = new Proxy(target, handler)
target
– je objekt, který lze zabalit, může to být cokoliv, včetně funkcí.handler
– konfigurace proxy:objekt s „pastmi“, metodami, které zachycují operace. – např.get
past pro čtení vlastnostitarget
,set
past pro zápis vlastnosti dotarget
, a tak dále.
Pro operace na proxy
, pokud je v handler
odpovídající past , pak se spustí a proxy má šanci to zvládnout, jinak se operace provede na target
.
Jako výchozí příklad vytvoříme proxy bez jakýchkoliv pastí:
let target = {};
let proxy = new Proxy(target, {}); // empty handler
proxy.test = 5; // writing to proxy (1)
alert(target.test); // 5, the property appeared in target!
alert(proxy.test); // 5, we can read it from proxy too (2)
for(let key in proxy) alert(key); // test, iteration works (3)
Protože zde nejsou žádné pasti, všechny operace na proxy
jsou přesměrovány na target
.
- Operace zápisu
proxy.test=
nastaví hodnotu natarget
. - Operace čtení
proxy.test
vrátí hodnotu ztarget
. - Iterace přes
proxy
vrátí hodnoty ztarget
.
Jak vidíme, bez jakýchkoliv pastí, proxy
je průhledný obal kolem target
.
Proxy
je zvláštní „exotický předmět“. Nemá vlastní vlastnosti. S prázdným handler
transparentně předává operace na target
.
Chcete-li aktivovat další schopnosti, přidejte pasti.
Co s nimi můžeme zachytit?
Pro většinu operací s objekty existuje ve specifikaci JavaScriptu takzvaná „interní metoda“, která popisuje, jak funguje na nejnižší úrovni. Například [[Get]]
, interní metoda pro čtení vlastnosti, [[Set]]
, interní metoda zápisu vlastnosti a tak dále. Tyto metody jsou použity pouze ve specifikaci, nemůžeme je volat přímo jménem.
Proxy pasti zachycují volání těchto metod. Jsou uvedeny ve specifikaci proxy a v tabulce níže.
Pro každou interní metodu je v této tabulce past:název metody, kterou můžeme přidat do handler
parametr new Proxy
k zastavení operace:
Interní metoda | Metoda manipulátoru | Spustí se, když… |
---|---|---|
[[Get]] | get | čtení vlastnosti |
[[Set]] | set | zápis do vlastnosti |
[[HasProperty]] | has | in operátor |
[[Delete]] | deleteProperty | delete operátor |
[[Call]] | apply | volání funkce |
[[Construct]] | construct | new operátor |
[[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 vynucuje některé invarianty – podmínky, které musí splňovat interní metody a pasti.
Většina z nich je pro návratové hodnoty:
[[Set]]
musí vrátittrue
pokud byla hodnota zapsána úspěšně, jinakfalse
.[[Delete]]
musí vrátittrue
pokud byla hodnota úspěšně smazána, jinakfalse
.- …a tak dále, více uvidíme v příkladech níže.
Existují některé další invarianty, například:
[[GetPrototypeOf]]
, aplikovaný na objekt proxy musí vrátit stejnou hodnotu jako[[GetPrototypeOf]]
použito na cílový objekt proxy objektu. Jinými slovy, čtení prototypu proxy musí vždy vrátit prototyp cílového objektu.
Pasti mohou tyto operace zachytit, ale musí dodržovat tato pravidla.
Invarianty zajišťují správné a konzistentní chování jazykových prvků. Úplný seznam invariantů je ve specifikaci. Pravděpodobně je neporušíte, pokud neděláte něco divného.
Podívejme se, jak to funguje na praktických příkladech.
Výchozí hodnota s pastí „get“
Nejběžnější pasti jsou pro vlastnosti čtení/zápisu.
Chcete-li zastavit čtení, handler
by měl mít metodu get(target, property, receiver)
.
Spustí se při čtení vlastnosti s následujícími argumenty:
target
– je cílový objekt, ten předaný jako první argument donew Proxy
,property
– název vlastnosti,receiver
– pokud je cílová vlastnost getter, pakreceiver
je objekt, který bude použit jakothis
ve svém volání. Obvykle je toproxy
samotný objekt (nebo objekt, který z něj dědí, pokud dědíme od proxy). Momentálně tento argument nepotřebujeme, takže bude podrobněji vysvětlen později.
Použijme get
implementovat výchozí hodnoty pro objekt.
Vytvoříme číselné pole, které vrátí 0
pro neexistující hodnoty.
Obvykle, když se někdo pokusí získat neexistující položku pole, dostane undefined
, ale do proxy zabalíme běžné pole, které zachytí čtení a vrátí 0
pokud taková vlastnost neexistuje:
let numbers = [0, 1, 2];
numbers = new Proxy(numbers, {
get(target, prop) {
if (prop in target) {
return target[prop];
} else {
return 0; // default value
}
}
});
alert( numbers[1] ); // 1
alert( numbers[123] ); // 0 (no such item)
Jak vidíme, s get
je to docela snadné past.
Můžeme použít Proxy
implementovat jakoukoli logiku pro „výchozí“ hodnoty.
Představte si, že máme slovník s frázemi a jejich překlady:
let dictionary = {
'Hello': 'Hola',
'Bye': 'Adiós'
};
alert( dictionary['Hello'] ); // Hola
alert( dictionary['Welcome'] ); // undefined
Právě teď, pokud není žádná fráze, čte se z dictionary
vrátí undefined
. V praxi je však obvykle lepší nechat frázi nepřeloženou než undefined
. Udělejme tedy, že v takovém případě vrátí nepřeloženou frázi namísto undefined
.
Abychom toho dosáhli, zabalíme dictionary
v proxy, která zachycuje operace čtení:
let dictionary = {
'Hello': 'Hola',
'Bye': 'Adiós'
};
dictionary = new Proxy(dictionary, {
get(target, phrase) { // intercept reading a property from dictionary
if (phrase in target) { // if we have it in the dictionary
return target[phrase]; // return the translation
} else {
// otherwise, return the non-translated phrase
return phrase;
}
}
});
// Look up arbitrary phrases in the dictionary!
// At worst, they're not translated.
alert( dictionary['Hello'] ); // Hola
alert( dictionary['Welcome to Proxy']); // Welcome to Proxy (no translation)
Poznámka:Všimněte si prosím, jak proxy přepisuje proměnnou:
dictionary = new Proxy(dictionary, ...);
Proxy by měl všude zcela nahradit cílový objekt. Nikdo by nikdy neměl odkazovat na cílový objekt poté, co byl zprostředkován. Jinak je snadné to pokazit.
Ověření pomocí „nastavené“ pasti
Řekněme, že chceme pole výhradně pro čísla. Pokud je přidána hodnota jiného typu, mělo by dojít k chybě.
set
trap se spouští při zápisu vlastnosti.
set(target, property, value, receiver)
:
target
– je cílový objekt, ten předaný jako první argument donew Proxy
,property
– název vlastnosti,value
– hodnota nemovitosti,receiver
– podobně jakoget
past, záleží pouze na vlastnostech setra.
set
past by měla vrátit true
pokud je nastavení úspěšné, a false
jinak (spustí TypeError
).
Použijme to k ověření nových hodnot:
let numbers = [];
numbers = new Proxy(numbers, { // (*)
set(target, prop, val) { // to intercept property writing
if (typeof val == 'number') {
target[prop] = val;
return true;
} else {
return false;
}
}
});
numbers.push(1); // added successfully
numbers.push(2); // added successfully
alert("Length is: " + numbers.length); // 2
numbers.push("test"); // TypeError ('set' on proxy returned false)
alert("This line is never reached (error in the line above)");
Upozornění:vestavěná funkčnost polí stále funguje! Hodnoty se přidávají po push
. length
vlastnost se po přidání hodnot automaticky zvýší. Náš proxy nic neporušuje.
Metody pole s přidanou hodnotou, jako je push
, nemusíme přepisovat a unshift
, a tak dále, aby tam přidali kontroly, protože interně používají [[Set]]
operace, která je zachycena serverem proxy.
Kód je tedy čistý a stručný.
Nezapomeňte vrátittrue
Jak bylo řečeno výše, existují invarianty, které je třeba držet.
Pro set
, musí vrátit true
pro úspěšné psaní.
Pokud to zapomeneme udělat nebo vrátíme nějakou falešnou hodnotu, operace spustí TypeError
.
Iterace pomocí „ownKeys“ a „getOwnPropertyDescriptor“
Object.keys
, for..in
smyčka a většina dalších metod, které iterují vlastnosti objektu, používají [[OwnPropertyKeys]]
interní metoda (zachycena ownKeys
trap), abyste získali seznam vlastností.
Tyto metody se v detailech liší:
Object.getOwnPropertyNames(obj)
vrátí nesymbolové klíče.Object.getOwnPropertySymbols(obj)
vrátí symbolové klíče.Object.keys/values()
vrátí nesymbolové klíče/hodnoty senumerable
příznak (příznaky vlastností byly vysvětleny v článku Příznaky a deskriptory vlastností).for..in
smyčky přes nesymbolové klávesy senumerable
vlajka a také prototypové klíče.
…Ale všechny začínají tímto seznamem.
V níže uvedeném příkladu používáme ownKeys
past na vytvoření for..in
smyčka přes user
a také Object.keys
a Object.values
, chcete-li přeskočit vlastnosti začínající podtržítkem _
:
let user = {
name: "John",
age: 30,
_password: "***"
};
user = new Proxy(user, {
ownKeys(target) {
return Object.keys(target).filter(key => !key.startsWith('_'));
}
});
// "ownKeys" filters out _password
for(let key in user) alert(key); // name, then: age
// same effect on these methods:
alert( Object.keys(user) ); // name,age
alert( Object.values(user) ); // John,30
Zatím to funguje.
I když, pokud vrátíme klíč, který v objektu neexistuje, Object.keys
neuvede to:
let user = { };
user = new Proxy(user, {
ownKeys(target) {
return ['a', 'b', 'c'];
}
});
alert( Object.keys(user) ); // <empty>
Proč? Důvod je jednoduchý:Object.keys
vrátí pouze vlastnosti s enumerable
vlajka. Pro kontrolu zavolá interní metodu [[GetOwnProperty]]
pro každou vlastnost získat svůj deskriptor. A protože zde není žádná vlastnost, její deskriptor je prázdný, žádné enumerable
vlajka, takže je přeskočena.
Pro Object.keys
abychom vrátili vlastnost, potřebujeme, aby buď existovala v objektu, s enumerable
příznak, nebo můžeme zachytit volání na [[GetOwnProperty]]
(past getOwnPropertyDescriptor
udělá) a vrátí deskriptor s enumerable: true
.
Zde je příklad:
let user = { };
user = new Proxy(user, {
ownKeys(target) { // called once to get a list of properties
return ['a', 'b', 'c'];
},
getOwnPropertyDescriptor(target, prop) { // called for every property
return {
enumerable: true,
configurable: true
/* ...other flags, probable "value:..." */
};
}
});
alert( Object.keys(user) ); // a, b, c
Poznamenejme ještě jednou:potřebujeme pouze zachytit [[GetOwnProperty]]
pokud vlastnost v objektu chybí.
Vlastnosti chráněné pomocí „deleteProperty“ a dalších pastí
Existuje rozšířená konvence, že vlastnosti a metody mají předponu podtržítkem _
jsou interní. Neměly by být přístupné zvenčí objektu.
Technicky je to však možné:
let user = {
name: "John",
_password: "secret"
};
alert(user._password); // secret
Abychom zabránili jakémukoli přístupu k vlastnostem začínajícím _
, použijte proxy .
Budeme potřebovat pasti:
get
vyvolat chybu při čtení takové vlastnosti,set
vyvolat chybu při zápisu,deleteProperty
vyvolat chybu při mazání,ownKeys
pro vyloučení vlastností začínajících_
odfor..in
a metody jakoObject.keys
.
Zde je kód:
let user = {
name: "John",
_password: "***"
};
user = new Proxy(user, {
get(target, prop) {
if (prop.startsWith('_')) {
throw new Error("Access denied");
}
let value = target[prop];
return (typeof value === 'function') ? value.bind(target) : value; // (*)
},
set(target, prop, val) { // to intercept property writing
if (prop.startsWith('_')) {
throw new Error("Access denied");
} else {
target[prop] = val;
return true;
}
},
deleteProperty(target, prop) { // to intercept property deletion
if (prop.startsWith('_')) {
throw new Error("Access denied");
} else {
delete target[prop];
return true;
}
},
ownKeys(target) { // to intercept property list
return Object.keys(target).filter(key => !key.startsWith('_'));
}
});
// "get" doesn't allow to read _password
try {
alert(user._password); // Error: Access denied
} catch(e) { alert(e.message); }
// "set" doesn't allow to write _password
try {
user._password = "test"; // Error: Access denied
} catch(e) { alert(e.message); }
// "deleteProperty" doesn't allow to delete _password
try {
delete user._password; // Error: Access denied
} catch(e) { alert(e.message); }
// "ownKeys" filters out _password
for(let key in user) alert(key); // name
Všimněte si prosím důležitého detailu v get
past v řádku (*)
:
get(target, prop) {
// ...
let value = target[prop];
return (typeof value === 'function') ? value.bind(target) : value; // (*)
}
Proč potřebujeme funkci k volání value.bind(target)
?
Důvodem je, že objektové metody, jako je user.checkPassword()
, musí mít přístup k _password
:
user = {
// ...
checkPassword(value) {
// object method must be able to read _password
return value === this._password;
}
}
Volání na číslo user.checkPassword()
získá proxy user
jako this
(objekt před tečkou se změní na this
), takže když se pokusí o přístup k this._password
, get
trap se aktivuje (spustí se při každém čtení vlastnosti) a vyvolá chybu.
Takže navážeme kontext objektových metod na původní objekt, target
, v řádku (*)
. Jejich budoucí volání pak budou používat target
jako this
, bez jakýchkoliv pastí.
Toto řešení obvykle funguje, ale není ideální, protože metoda může předat objekt bez proxy někam jinam a pak se dostaneme do nepořádku:kde je původní objekt a kde ten zastoupený?
Kromě toho může být objekt vícekrát zprostředkován proxy (více proxy může k objektu přidat různé „vychytávky“), a pokud předáme nezabalený objekt metodě, může to mít neočekávané následky.
Takový proxy by se tedy neměl používat všude.
Soukromé vlastnosti třídy
Moderní stroje JavaScript nativně podporují soukromé vlastnosti ve třídách s předponou #
. Jsou popsány v článku Soukromé a chráněné vlastnosti a metody. Nejsou vyžadovány žádné proxy.
Takové vlastnosti však mají své vlastní problémy. Zejména se nedědí.
„V dosahu“ s pastí „má“
Podívejme se na další příklady.
Máme objekt rozsahu:
let range = {
start: 1,
end: 10
};
Rádi bychom použili in
operátor pro kontrolu, zda je číslo v range
.
has
past zachytí in
hovory.
has(target, property)
target
– je cílový objekt, předaný jako první argument donew Proxy
,property
– název vlastnosti
Zde je ukázka:
let range = {
start: 1,
end: 10
};
range = new Proxy(range, {
has(target, prop) {
return prop >= target.start && prop <= target.end;
}
});
alert(5 in range); // true
alert(50 in range); // false
Pěkný syntaktický cukr, že? A velmi jednoduché na implementaci.
Funkce obtékání:"použít"
Můžeme také obklopit funkci proxy.
apply(target, thisArg, args)
trap zpracovává volání proxy jako funkce:
target
je cílový objekt (funkce je objekt v JavaScriptu),thisArg
je hodnotathis
.args
je seznam argumentů.
Vzpomeňme například delay(f, ms)
dekoratér, který jsme provedli v článku Dekoratéři a přeposílání, zavolejte/přihlaste se.
V tom článku jsme to udělali bez proxy. Volání na číslo delay(f, ms)
vrátil funkci, která přesměruje všechna volání na f
po ms
milisekund.
Zde je předchozí implementace založená na funkcích:
function delay(f, ms) {
// return a wrapper that passes the call to f after the timeout
return function() { // (*)
setTimeout(() => f.apply(this, arguments), ms);
};
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
// after this wrapping, calls to sayHi will be delayed for 3 seconds
sayHi = delay(sayHi, 3000);
sayHi("John"); // Hello, John! (after 3 seconds)
Jak jsme již viděli, většinou to funguje. Funkce wrapper (*)
provede hovor po vypršení časového limitu.
Ale funkce wrapper nepředává operace čtení/zápisu vlastnosti ani nic jiného. Po zabalení se ztratí přístup k vlastnostem původních funkcí, jako je name
, length
a další:
function delay(f, ms) {
return function() {
setTimeout(() => f.apply(this, arguments), ms);
};
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
alert(sayHi.length); // 1 (function length is the arguments count in its declaration)
sayHi = delay(sayHi, 3000);
alert(sayHi.length); // 0 (in the wrapper declaration, there are zero arguments)
Proxy
je mnohem výkonnější, protože vše předává cílovému objektu.
Použijme Proxy
místo funkce zalamování:
function delay(f, ms) {
return new Proxy(f, {
apply(target, thisArg, args) {
setTimeout(() => target.apply(thisArg, args), ms);
}
});
}
function sayHi(user) {
alert(`Hello, ${user}!`);
}
sayHi = delay(sayHi, 3000);
alert(sayHi.length); // 1 (*) proxy forwards "get length" operation to the target
sayHi("John"); // Hello, John! (after 3 seconds)
Výsledek je stejný, ale nyní jsou nejen volání, ale všechny operace na proxy přesměrovány na původní funkci. Takže sayHi.length
je vráceno správně po zalomení v řádku (*)
.
Máme „bohatší“ obal.
Existují i další pasti:úplný seznam je na začátku tohoto článku. Jejich způsob použití je podobný výše uvedenému.
Reflexe
Reflect
je vestavěný objekt, který zjednodušuje vytváření Proxy
.
Dříve bylo řečeno, že interní metody, jako je [[Get]]
, [[Set]]
a další jsou pouze pro specifikaci, nelze je volat přímo.
Reflect
objekt to do jisté míry umožňuje. Jeho metody jsou minimální obaly kolem interních metod.
Zde jsou příklady operací a Reflect
volání, která dělají totéž:
Operace | Reflect zavolat | Interní metoda |
---|---|---|
obj[prop] | Reflect.get(obj, prop) | [[Get]] |
obj[prop] = value | Reflect.set(obj, prop, value) | [[Set]] |
delete obj[prop] | Reflect.deleteProperty(obj, prop) | [[Delete]] |
new F(value) | Reflect.construct(F, value) | [[Construct]] |
… | … | … |
Například:
let user = {};
Reflect.set(user, 'name', 'John');
alert(user.name); // John
Konkrétně Reflect
nám umožňuje volat operátorům (new
, delete
…) jako funkce (Reflect.construct
, Reflect.deleteProperty
, …). To je zajímavá schopnost, ale zde je důležitá jiná věc.
Pro každou interní metodu lze zachytit pomocí Proxy
, v Reflect
je odpovídající metoda , se stejným názvem a argumenty jako Proxy
past.
Můžeme tedy použít Reflect
pro předání operace původnímu objektu.
V tomto příkladu jsou obě depeše get
a set
transparentně (jako by neexistovaly) přeposílat operace čtení/zápisu objektu a zobrazovat zprávu:
let user = {
name: "John",
};
user = new Proxy(user, {
get(target, prop, receiver) {
alert(`GET ${prop}`);
return Reflect.get(target, prop, receiver); // (1)
},
set(target, prop, val, receiver) {
alert(`SET ${prop}=${val}`);
return Reflect.set(target, prop, val, receiver); // (2)
}
});
let name = user.name; // shows "GET name"
user.name = "Pete"; // shows "SET name=Pete"
Zde:
Reflect.get
čte vlastnost objektu.Reflect.set
zapíše vlastnost objektu a vrátítrue
v případě úspěchufalse
jinak.
To znamená, že vše je jednoduché:pokud chce past přesměrovat hovor na objekt, stačí zavolat Reflect.<method>
se stejnými argumenty.
Ve většině případů můžeme udělat totéž bez Reflect
, například čtení vlastnosti Reflect.get(target, prop, receiver)
lze nahradit target[prop]
. Existují však důležité nuance.
Zastupování getteru
Podívejme se na příklad, který ukazuje, proč Reflect.get
je lepší. A také uvidíme, proč get/set
mají třetí argument receiver
, které jsme dříve nepoužívali.
Máme objekt user
s _name
majetek a jeho příjemce.
Tady je proxy:
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) {
return target[prop];
}
});
alert(userProxy.name); // Guest
get
past je zde „transparentní“, vrací původní vlastnost a nedělá nic jiného. Pro náš příklad to stačí.
Všechno se zdá být v pořádku. Udělejme však příklad trochu složitější.
Po zdědění jiného objektu admin
od user
, můžeme pozorovat nesprávné chování:
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) {
return target[prop]; // (*) target = user
}
});
let admin = {
__proto__: userProxy,
_name: "Admin"
};
// Expected: Admin
alert(admin.name); // outputs: Guest (?!?)
Čtení admin.name
by měl vrátit "Admin"
, nikoli "Guest"
!
Co se děje? Možná jsme udělali něco špatně s dědictvím?
Ale pokud odstraníme proxy, vše bude fungovat podle očekávání.
Problém je ve skutečnosti v proxy, v řádku (*)
.
-
Když čteme
admin.name
, jakoadmin
objekt takovou vlastní vlastnost nemá, hledání přejde na jeho prototyp. -
Prototyp je
userProxy
. -
Při čtení
name
vlastnost z proxy, jehoget
trap spustí a vrátí jej z původního objektu jakotarget[prop]
v řádku(*)
.Volání na číslo
target[prop]
, kdyžprop
je getter, spouští svůj kód v kontextuthis=target
. Takže výsledek jethis._name
z původního objektutarget
, to znamená:zuser
.
K nápravě takových situací potřebujeme receiver
, třetí argument z get
past. Zachovává správnou hodnotu this
být předán getteru. V našem případě je to admin
.
Jak předat kontext getteru? Pro běžnou funkci bychom mohli použít call/apply
, ale to je getter, není to „volané“, pouze přístupné.
Reflect.get
může to udělat. Všechno bude fungovat správně, pokud to použijeme.
Zde je opravená varianta:
let user = {
_name: "Guest",
get name() {
return this._name;
}
};
let userProxy = new Proxy(user, {
get(target, prop, receiver) { // receiver = admin
return Reflect.get(target, prop, receiver); // (*)
}
});
let admin = {
__proto__: userProxy,
_name: "Admin"
};
alert(admin.name); // Admin
Nyní receiver
který uchovává odkaz na správný this
(to je admin
), je předán getteru pomocí Reflect.get
v řádku (*)
.
Past můžeme přepsat ještě kratší:
get(target, prop, receiver) {
return Reflect.get(...arguments);
}
Reflect
volání jsou pojmenována přesně stejným způsobem jako depeše a přijímají stejné argumenty. Byly speciálně navrženy tímto způsobem.
Takže return Reflect...
poskytuje bezpečný ne-přemýšlivý způsob, jak přeposlat operaci a zajistit, abychom na nic souvisejícího nezapomněli.
Omezení proxy
Proxy poskytují jedinečný způsob, jak změnit nebo vyladit chování existujících objektů na nejnižší úrovni. Přesto to není dokonalé. Existují omezení.
Vestavěné objekty:Vnitřní sloty
Mnoho vestavěných objektů, například Map
, Set
, Date
, Promise
a další využívají takzvané „interní sloty“.
Jsou to podobné vlastnosti, ale vyhrazené pro interní účely pouze pro specifikaci. Například Map
ukládá položky do vnitřního slotu [[MapData]]
. Vestavěné metody k nim přistupují přímo, nikoli přes [[Get]]/[[Set]]
interní metody. Takže Proxy
nemůže to zachytit.
Proč se starat? Stejně jsou interní!
No, tady je problém. Po připojení takového vestavěného objektu proxy server proxy nemá tyto interní sloty, takže vestavěné metody selžou.
Například:
let map = new Map();
let proxy = new Proxy(map, {});
proxy.set('test', 1); // Error
Interně Map
ukládá všechna data do svého [[MapData]]
vnitřní slot. Proxy takový slot nemá. Vestavěná metoda Map.prototype.set
metoda se pokouší o přístup k vnitřní vlastnosti this.[[MapData]]
, ale protože this=proxy
, nelze jej najít v proxy
a prostě selže.
Naštěstí existuje způsob, jak to opravit:
let map = new Map();
let proxy = new Proxy(map, {
get(target, prop, receiver) {
let value = Reflect.get(...arguments);
return typeof value == 'function' ? value.bind(target) : value;
}
});
proxy.set('test', 1);
alert(proxy.get('test')); // 1 (works!)
Nyní to funguje dobře, protože get
trap váže vlastnosti funkce, jako je map.set
, na cílový objekt (map
).
Na rozdíl od předchozího příkladu hodnota this
uvnitř proxy.set(...)
nebude proxy
, ale původní map
. Takže když interní implementace set
pokusí o přístup k this.[[MapData]]
interní slot, je to úspěšné.
Array
nemá žádné vnitřní sloty
Pozoruhodná výjimka:vestavěný Array
nepoužívá vnitřní sloty. Je to z historických důvodů, jak se zdálo tak dávno.
Při proxy poli tedy žádný takový problém není.
Soukromá pole
Podobná věc se stane s poli soukromé třídy.
Například getName()
metoda přistupuje k soukromému #name
vlastnost a přestávky po proxy:
class User {
#name = "Guest";
getName() {
return this.#name;
}
}
let user = new User();
user = new Proxy(user, {});
alert(user.getName()); // Error
Důvodem je, že soukromá pole jsou implementována pomocí interních slotů. JavaScript nepoužívá [[Get]]/[[Set]]
při přístupu k nim.
Ve volání getName()
hodnotu this
je proxy user
a nemá slot se soukromými poli.
Opět platí, že řešení s vázáním metody to umožňuje:
class User {
#name = "Guest";
getName() {
return this.#name;
}
}
let user = new User();
user = new Proxy(user, {
get(target, prop, receiver) {
let value = Reflect.get(...arguments);
return typeof value == 'function' ? value.bind(target) : value;
}
});
alert(user.getName()); // Guest
Jak již bylo řečeno, řešení má nevýhody, jak bylo vysvětleno dříve:vystavuje původní objekt metodě, což potenciálně umožňuje jeho další předání a narušuje další funkce proxy.
Proxy !=cíl
Proxy a původní objekt jsou různé objekty. To je přirozené, že?
Pokud tedy jako klíč použijeme původní objekt a poté jej zastoupíme proxy, nelze proxy najít:
let allUsers = new Set();
class User {
constructor(name) {
this.name = name;
allUsers.add(this);
}
}
let user = new User("John");
alert(allUsers.has(user)); // true
user = new Proxy(user, {});
alert(allUsers.has(user)); // false
Jak vidíme, po použití proxy nemůžeme najít user
v sadě allUsers
, protože proxy je jiný objekt.
===
Proxy mohou zachytit mnoho operátorů, například new
(s construct
), in
(s has
), delete
(s deleteProperty
) a tak dále.
Ale neexistuje způsob, jak zachytit přísný test rovnosti objektů. Objekt je striktně roven sám sobě a nemá žádnou jinou hodnotu.
Takže všechny operace a vestavěné třídy, které porovnávají objekty z hlediska rovnosti, budou rozlišovat mezi objektem a proxy. Žádná transparentní náhrada zde není.
Odvolatelné proxy
odvolatelný proxy je proxy, kterou lze zakázat.
Řekněme, že máme zdroj a chtěli bychom k němu kdykoli uzavřít přístup.
Co můžeme udělat, je zabalit to do odvolatelné proxy, bez jakýchkoliv pastí. Takový proxy bude předávat operace objektu a my ho můžeme kdykoli zakázat.
Syntaxe je:
let {proxy, revoke} = Proxy.revocable(target, handler)
Volání vrátí objekt s proxy
a revoke
funkci zakázat.
Zde je příklad:
let object = {
data: "Valuable data"
};
let {proxy, revoke} = Proxy.revocable(object, {});
// pass the proxy somewhere instead of object...
alert(proxy.data); // Valuable data
// later in our code
revoke();
// the proxy isn't working any more (revoked)
alert(proxy.data); // Error
Volání na číslo revoke()
odstraní všechny interní odkazy na cílový objekt z proxy, takže již nejsou připojeny.
Zpočátku revoke
je oddělený od proxy
, abychom mohli předat proxy
při opuštění revoke
v aktuálním rozsahu.
Můžeme také svázat revoke
způsob na proxy nastavením proxy.revoke = revoke
.
Další možností je vytvořit WeakMap
který má proxy
jako klíč a odpovídající revoke
jako hodnotu, která umožňuje snadno najít revoke
pro proxy:
let revokes = new WeakMap();
let object = {
data: "Valuable data"
};
let {proxy, revoke} = Proxy.revocable(object, {});
revokes.set(proxy, revoke);
// ..somewhere else in our code..
revoke = revokes.get(proxy);
revoke();
alert(proxy.data); // Error (revoked)
Používáme WeakMap
místo Map
zde, protože nebude blokovat sběr odpadu. Pokud se objekt proxy stane „nedosažitelným“ (např. žádná proměnná na něj již neodkazuje), WeakMap
umožňuje jeho vymazání z paměti spolu s jeho revoke
že už nebudeme potřebovat.
Odkazy
- Specifikace:Proxy.
- MDN:Proxy.
Shrnutí
Proxy
je obal kolem objektu, který přeposílá operace s ním na objekt a případně některé z nich zachytí.
Může zabalit jakýkoli druh objektu, včetně tříd a funkcí.
Syntaxe je:
let proxy = new Proxy(target, {
/* traps */
});
…Pak bychom měli použít proxy
všude místo target
. Proxy nemá své vlastní vlastnosti ani metody. Zachytí operaci, pokud je poskytnuta, jinak ji předá na target
objekt.
Můžeme past:
- Čtení (
get
), psaní (set
), odstranění (deleteProperty
) vlastnost (i neexistující). - Volání funkce (
apply
past). new
operátor (construct
past).- Mnoho dalších operací (úplný seznam je na začátku článku a v dokumentech).
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é objekt několikrát zabalit do různých proxy serverů a ozdobit jej různými aspekty funkčnosti.
Reflect API je navrženo tak, aby doplňovalo proxy. Pro jakékoli Proxy
past, je tam Reflect
volat se stejnými argumenty. Měli bychom je použít k přesměrování volání na cílové objekty.
Proxy mají určitá omezení:
- Vestavěné objekty mají „vnitřní sloty“, přístup k nim nelze použít jako proxy. Viz výše uvedené řešení.
- Totéž platí pro pole soukromých tříd, protože jsou interně implementována pomocí slotů. Volání metody proxy musí mít cílový objekt jako
this
pro přístup k nim. - Testy rovnosti objektů
===
nelze zachytit. - Výkon:Srovnávací hodnoty závisí na motoru, ale obecně přístup ke službě pomocí nejjednoduššího proxy trvá několikrát déle. V praxi to však má význam pouze pro některé „úzké“ objekty.