Wprowadzenie w temat
Zarówno Proxy
, jak i Reflect
to wbudowane w JavaScript obiekty. Chociaż można ich używać zupełnie niezależnie (i czasami tak się robi), to najczęściej wykorzystuje się je razem, dlatego postanowiłem wspomnieć o obu, w jednym artykule.
Za chwilę omówimy je nieco bardziej szczegółowo i poznamy ich działanie w JS, ale zanim do tego przejdziemy, to poświęćmy chwilę na szybkie wprowadzenie.
W programowaniu termin proxy występuje bardzo często. Mówiąc ogólnie jest to wzorzec projektowy, który pozwala kontrolować dostęp (odczyt, zapis, modyfikację) do klasy, obiektu, itp. przy pomocy kodu, który "przechwytuje" wszelkie próby interakcji, a następnie decyduje, co powinno się stać z nimi dalej.
W przypadku JavaScriptu mechanizm jest podobny - Proxy
"opakowuje" inne obiekty, natomiast Reflect
zawiera statyczne metody, które mogą się okazać bardzo przydatne podczas pracy z danym obiektem.
Proxy
Jak wspomniałem wcześniej, Proxy
opakowuje inny obiekt. Wynikiem tej operacji jest utworzenie nowego obiektu, który pełni rolę mediatora pomiędzy obiektem opakowanym, a resztą kodu. Wszelkie próby dostępu do obiektu docelowego, muszą przejść przez proxy.
Można to zobrazować w dość prosty sposób:

Z perspektywy osoby, która będzie w przyszłości używać takiego obiektu, nic nie wskazuje na to, że ma do czynienia z proxy, a więc w żaden sposób nie utrudnia to pracy z takim obiektem.
Zanim przejdziemy do kodu, poruszę jeszcze jedną kwestię - oryginalny obiekt nie jest w żaden sposób zmodyfikowany i wciąż może być odczytany i zmodyfikowany bezpośrednio (bez udziału proxy), Proxy
tworzy nowy obiekt i to właśnie on będzie "chroniony".
W pierwszym przykładzie, skupimy się na podstawowej składni, a w kolejnych zaczniemy dodawać logikę do utworzonego obiektu.
Jak widzisz, mimo, że opakowałem obiekt person
w Proxy
, to na pierwszy rzut oka nic się nie zmieniło - nadal mogę bez problem odczytać i zmodyfikować właściwości z nowo utworzonego obiektu.
W praktyce proxy już działa, jednak w żaden sposób nie modyfikuje przechwytywanych operacji, dlatego z naszej perspektywy nic się nie zmieniło.
Zacznijmy od przechwytywania operacji odczytania właściwości z obiektu. Żeby to zrobić musimy nieco zmodyfikować drugi argument - aktualnie jest to pusty obiekt, ale możemy utworzyć w nim metody, które będą wywoływane w momencie wystąpienia jakiegoś działania (np. odczytu właściwości).
W świecie JS, te metody są znane pod nazwą traps, czyli pułapki, ponieważ "wpadają" w nie wszelkie próby interakcji z obiektem docelowym.
Zanim jednak napiszemy jakiś kod, musisz wiedzieć o bardzo ważnej rzeczy. Pułapki nadpisują domyślne zachowanie obiektu, oznacza to, że jeśli chcesz jedynie dodać jakieś zachowanie, to musisz również odtworzyć standardowe działanie obiektu.
Oczywiście nie zawsze musi to być Twoim celem i czasami (bardzo rzadko) zdarza się, że chcemy zupełnie zmienić działanie jakiegoś mechanizmu (odczytu, modyfikacji, itd.).
Czas na wyjaśnienie krok, po kroku, co dzieje się w tym przykładzie:
Tworzymy nowy obiekt
person
, który następnie opakowujemy wProxy
Jako drugi argument do konstruktura
Proxy
przekazujemy obiekt, zawierający metody. W tym przykładzie używamy jedynieget
, a jej nazwa nie jest przypadkowa i musi taka pozostać (w przeciwnym razie metoda nie będzie wywołana i pułapka nie będzie stworzona poprawnie)Metoda
get
otrzymuje dwa argumentytarget
(czyli obiekt docelowy), orazproperty
(odczytywany klucz).Następnie wyświetlam w konsoli drugi argument -
property
Ostatni krok to odtworzenie domyślnego zachowania - w przypadku odczytywania właściwości, spodziewamy się otrzymać jakąś wartość, zatem zwracam
target[property]
, czyli poszukiwaną właściwość. Gdybym tego nie zrobił, każda próba odczytania właściwości z obiektu zwracałabyundefined
, zamiast poprawnej wartości.
Oczywiście get
nie jest jedyną dostępną pułapką, ale zanim poznamy kolejne, chciałem pokazać jeszcze jeden przykład - tym razem nieco bardziej praktyczny.
Dodamy do obiektu możliwość odczytania kilku kluczy jednocześnie używając następującej składni obj["key1;key2"]
. Zachowamy także domyślne zachowanie.
Kod w tym przykładzie w dużej mierze przypomina ten, z pierwszego przykładu. Nadal stosujemy pułapkę get
, i nadal zachowujemy domyślne zachowanie.
Jednak zanim zwrócimy wartość, dokonujemy sprawdzenia, czy nie został podany ciąg wielu kluczy. Jeżeli tablica keys
ma więcej niż jeden element, oznacza to, że próbujemy odczytać kilka właściwości jednocześnie. W tym przypadku zamiast zwracać pojedynczą wartość, zwracamy tablicę, która zawiera wszystkie odczytywane właściwości.
Drugą najczęściej stosowaną pułapką jest set
. Przechwytuje ona operacje modyfikujące właściwości obiektu. Dzięki temu możemy np. przeprowadzić walidację nowej wartości, zanim przypiszemy ją w miejsce istniejącej. Zastosowań jest jednak znacznie więcej - w przykładzie poniżej zaimplementujemy mechanizm, który pozwala zmienić właściwości obiektu tylko raz, każda kolejna próba rzuci wyjątek.
Zwróć uwagę, że ta pułapka posiada dodatkowy parametr - value
. Trafi do niego wartość, która jest przypisywana, np. person.age = 35
(value przyjmie wartość 35
).
Do tej pory używaliśmy tylko jednej pułapki w obiekcie, jednak nic nie stoi na przeszkodzie, żeby użyć ich więcej i przechwytywać jednocześnie np. odczyt oraz modyfikację.
Pozostałe pułapki
W tym artykule skupiam się na dwóch najczęściej stosowanych pułapkach - set
oraz get
, ale warto wiedzieć, że jest ich znacznie więcej, a każda z nich, pozwala na kontrolowanie innej "części" obiektu.
W tabeli poniżej wymienione są wszystkie dostępne możliwości, wraz z opisem, kiedy zostaną uruchomione.
Nazwa pułapki | Co ją uruchamia |
---|---|
get | odczyt właściwości obiektu |
set | modyfikacja, lub dodanie nowej właściwości do obiektu |
has | użycie operatora in, do sprawdzenia, czy dana właściwość występuje w obiekcie |
apply | wywołanie metody z danego obiektu |
deleteProperty | użycia operatora delete do usunięcia właściwości z obiektu. |
construct | zastosowanie operatora new |
getPrototypeOf | zastosowanie metody Object.getPrototypeOf |
setPrototypeOf | zastosowanie metody Object.setPrototypeOf |
isExtensible | zastosowanie metody Object.isExtensible |
preventExtensions | zastosowanie metody Object.preventExtensions |
defineProperty | zastosowanie metody Object.defineProperty lub Object.defineProperties |
getOwnPropertyDescriptor | zastosowanie metody Object.getOwnPropertyDescriptor, pętli |
ownKeys | zastosowanie metod Object.getOwnPropertyNames, Object.getOwnPropertySymbols, |
Pamiętaj, że pułapki mogą otrzymywać różna liczbę argumentów, a także różne wartości w argumentach. Koniecznie upewnij się, co jest dostępne w danej pułapce, zanim z niej skorzystasz.
Co prawda nie ma żadnego ograniczenia, co do liczby pułapek, którą możesz użyć wewnątrz jednego obiektu Proxy, ale pamiętaj, że modyfikowane domyślnych zachowań, do których inni programiści są przyzwyczajenia, może nie być dobrym pomysłem. W szczególności, gdy zmodyfikujesz wszystko, co tylko się da w obiekcie.
Może dojść do sytuacji, w której praca z nim jest po prostu nieintuicyjna, a Proxy, zamiast pomóc, zaczęło po prostu przeszkadzać.
Podsumowując: stosuj pułapki z umiarem i unikaj całkowitego nadpisania domyślnego zachowania obiektu.
Reflect w połączeniu z Proxy
W poprzedniej części artykułu, kiedy opisywałem obiekt Proxy
, wspomniałem o tym, że pułapki nadpisują domyślne zachowanie obiektu, a jeżeli chcemy je zachować, to należy je ręcznie stworzyć od nowa.
Jest to na tyle częsty przypadek, że twórcy języka dostarczyli specjalny obiekt - Reflect
, który ma na celu ułatwienie tego zadania.
Zanim jednak przejdziemy do samego kodu, to na wstępie trzeba zaznaczyć, że Reflect
posiada jedynie statyczne metody (podobnie jak Math
) i nie można utworzyć jego instancji z użyciem słowa kluczowego new
.
Skoro teoria za nami, to czas na przykład - tym razem stworzymy nieco bardziej rozbudowany obiekt Proxy
, a do zachowania domyślnego działania użyjemy metod z obiektu Reflect
.
W tym przykładzie używam dwóch pułapek:
get
- zwraca string"unknown"
, jeżeli szukany klucz ma przypisaną wartośćundefined
, lub w ogóle nie istnieje. W pozostałych przypadkach zachowujemy domyślne działanie.set
- sprawdza, czy nie próbujemy przypisaćundefined
, jako wartość. Jeżeli tak, zwracany jest błąd. W pozostałych przypadkach zachowujemy domyślne działanie.
Obiekt Reflect
posiada odpowiednią metodę dla każdej pułapki, wymienionej w tabeli wyżej (np. dla getPrototypeOf
jest to Reflect.getPrototypeOf
. Oznacza to, że niezależnie od tego, jakie zachowanie chcesz zmodyfikować, to zawsze możesz również odtworzyć domyślne zachowanie z użyciem Reflect
i nie musisz robić tego "ręcznie" 😎
Podsumowanie
Proxy jest niezwykle przydatnym mechanizmem - pozwala na modyfikację domyślnych zachowań obiektu, dodając np. walidację właściwości, ograniczenia odczytu, czy logowanie danych statystycznych. Zastosowań jest wiele i jak to zazwyczaj w programowaniu bywa, ogranicza nas tylko wyobraźnia.
Zanim jednak zaczniesz używać Proxy wszędzie, gdzie tylko się da, to pamiętaj, że może to negatywnie wpłynąć na wydajność aplikacji, dlatego jeśli wydajność jest Twoim priorytetem, to koniecznie na bieżąco monitoruj wpływ Proxy na nią.
Jak to zwykle bywa, tak rozbudowanego tematu nie da się wyczerpać jednym artykułem, dlatego zachęcam do eksperymentowania i poszerzania wiedzy - to, co przedstawiłem, to jedynie wierzchołek góry lodowej.