Miniatura artykułu

Obiekty Proxy i Reflect

10 minut

Skopiuj link

Data publikacji: 10/29/2023, 7:24:35

Ostatnia aktualizacja: 4/1/2024

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:

  1. Tworzymy nowy obiekt person, który następnie opakowujemy w Proxy

  2. Jako drugi argument do konstruktura Proxy przekazujemy obiekt, zawierający metody. W tym przykładzie używamy jedynie get, 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)

  3. Metoda get otrzymuje dwa argumenty target (czyli obiekt docelowy), oraz property (odczytywany klucz).

  4. Następnie wyświetlam w konsoli drugi argument - property

  5. 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łaby undefined, 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 for...in, lub Object.keys, Object.values, Object.entries

ownKeys

zastosowanie metod Object.getOwnPropertyNamesObject.getOwnPropertySymbols, Object.keys, Object.values, Object.entries, lub pętli for...in

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.

Avatar: Wojciech Rygorowicz

Software Engineer / Fullstack developer

Wojciech Rygorowicz

wojciech.rygorowicz@gmail.com

Podziel się na

Dodaj komentarz

Komentarze (0)

Brak komentarzy