Proxy a reflexe

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í vlastnosti target , set past pro zápis vlastnosti do target , 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 .

  1. Operace zápisu proxy.test= nastaví hodnotu na target .
  2. Operace čtení proxy.test vrátí hodnotu z target .
  3. Iterace přes proxy vrátí hodnoty z target .

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
Invarianty

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átit true pokud byla hodnota zapsána úspěšně, jinak false .
  • [[Delete]] musí vrátit true pokud byla hodnota úspěšně smazána, jinak false .
  • …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 do new Proxy ,
  • property – název vlastnosti,
  • receiver – pokud je cílová vlastnost getter, pak receiver je objekt, který bude použit jako this ve svém volání. Obvykle je to proxy 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 do new Proxy ,
  • property – název vlastnosti,
  • value – hodnota nemovitosti,
  • receiver – podobně jako get 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átit true

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 s enumerable 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 s enumerable 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 _ od for..in a metody jako Object.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 do new 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 hodnota this .
  • 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ěchu false 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 (*) .

  1. Když čteme admin.name , jako admin objekt takovou vlastní vlastnost nemá, hledání přejde na jeho prototyp.

  2. Prototyp je userProxy .

  3. Při čtení name vlastnost z proxy, jeho get trap spustí a vrátí jej z původního objektu jako target[prop] v řádku (*) .

    Volání na číslo target[prop] , když prop je getter, spouští svůj kód v kontextu this=target . Takže výsledek je this._name z původního objektu target , to znamená:z user .

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 nemohou zachytit přísný test rovnosti ===

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.