Uniwersalny getter
Chociaż magia niezbyt idzie w parze z programowaniem, to mimo to polubiłem PHP-owe metody magiczne, wśród których chyba najbardziej przypadło mi do gustu __get
. Ta prosta metoda pozwalała przechwycić odwołania do nieistniejących pól klasy:
<?php
class Test {
public $iAmAlive = 'nope';
public function __get( $var ) {
return $var . ' value';
}
}
$test = new Test();
var_dump( $test->test ); // test value
var_dump( $test->qwerty ); // qwerty value
var_dump( $test->iAmAlive ); // nope
W przypadku JS-a nie było to, niestety, możliwe. Aż do ES6…
Nieuniwersalny getter
Pierwsze zarzewie tego typu mechanizmów pojawiło się w ES5, wraz z getterami. Niemniej można je było przypiąć tylko do konkretnej właściwości, co sprawiało, że getterów musiało być tyle, ile własności, które chcemy obserwować. Dodatkowo nie dało się ich przypiąć do nieistniejących właściwości:
const obj = {};
Object.defineProperty( obj, 'test', {
get() {
return 'hublabubla';
}
} );
console.log( obj.test ); // hublabubla
console.log( obj.whatever ); // undefined
Wynikało to z prostego faktu: getter był tak naprawdę cechą konkretnej właściwości obiektu.
Proxy
Naprawdę potężny mechanizm, Proxy
, który pozwala kontrolować dostęp do obiektu, pojawił się dopiero w ES6, wraz z towarzyszącym mu mechanizmem refleksji. Proxy
pozwala na tworzenie tzw. pułapek (ang. trap) – funkcji odpalanych w momencie zajścia konkretnej interakcji z obiektem skrytym za Proxy
. Stąd wzięła się też nazwa tego mechanizmu – Proxy
działa jak… proxy, pośrednicząc pomiędzy prawdziwym obiektem a próbującym go użyć programistą.
Pułapek jest naprawdę sporo i pozwalają kontrolować niemal każdą interakcję z obiektem (nawet jeśli chcemy kontrolować odczytywanie deskryptorów właściwości obiektu, to odpowiednia pułapka istnieje i na to). Niemniej nam przyda się tylko jedna – get
. Jak sama jej nazwa wskazuje, pułapka ta uaktywnia się, gdy ktoś chce się dostać do jakiejkolwiek właściwości naszego obiektu.
Zobaczmy zatem, jak ukryć obiekt za proxy:
const obj = {};
const proxy = new Proxy( obj, {} );
Obiekt, który chcemy ukryć, to obj
. Przekazujemy go jako pierwszy parametr do konstruktora Proxy
. Drugi parametr to tzw. handler, czyli obiekt zawierający definicję pułapek.
W powyższym wypadku nasze proxy nie robi absolutnie nic – przepuszcza wszystko do oryginalnego obiektu:
proxy.a = 'whatever'; // 1
console.log( obj.a ); // 2 – whatever
Jak widać ustawiamy właściwość na proxy (1), co automatycznie ustawia ją także w oryginalnym obiekcie (2).
Dodajmy zatem w końcu nasz getter.
Uniwersalny getter
Załóżmy, że chcemy, by każda właściwość naszego obiektu zwracała shruga ( ¯\_(ツ)_/¯
):
const obj = {};
const proxy = new Proxy( obj, {
get() {
return '¯\\_(ツ)_/¯';
}
} );
console.log( proxy.a ); // ¯\_(ツ)_/¯
I już, tyle. Teraz każda właściwość będzie zwracała shruga.
Warto zauważyć, że w przypadku używania proxy, wszystkie operacje wykonujemy właśnie na nim. W zależności od zdefiniowanych pułapek, nasze działania na proxy będą miały odpowiednie odbicie na rzeczywistym obiekcie.
I mówiąc każda mam na myśli naprawdę każdą:
const obj = {
a: 'whatever'
};
const proxy = new Proxy( obj, {
get() {
return '¯\\_(ツ)_/¯';
}
} );
console.log( proxy.a ); // ¯\_(ツ)_/¯
Pułapka get
– w przeciwieństwie do PHP-owego __get
– działa bowiem dla wszystkich właściwości danego obiektu, nawet tych zdefiniowanych. Tym samym wypada dodać trochę kodu, aby się przed tym uchronić.
Pułapka get
dostaje 3 parametry:
target
– nasz oryginalny obiekt;property
– nazwę właściwości, którą użytkownik chce odczytać;receiver
– obiekt proxy, który wywołał pułapkę.
Te informacje wystarczą nam aż nadto, by dorobić obsługę zdefiniowanych właściwości:
const obj = {
a: 'whatever'
};
const proxy = new Proxy( obj, {
get( target, property ) {
if ( Reflect.has( target, property ) ) { // 1
return Reflect.get( target, property ); // 2
}
return '¯\\_(ツ)_/¯'; // 3
}
} );
console.log( proxy.a ); // whatever
console.log( proxy.b ); // ¯\_(ツ)_/¯
Używamy tutaj mechanizmu refleksji do sprawdzenia, czy oryginalny obiekt ma odpowiednią właściwość (1), a jeśli tak – zwracamy ją (2). W innym wypadku zwracamy shruga (3).
Oczywiście można zastosować bardziej tradycyjne metody niż refleksja (for...in
i target[ property ]
), niemniej uważam, że refleksja jest po prostu czytelniejsza i bardziej elegancka. No i co najważniejsze: `Reflect` ma metody odpowiadające pułapkom `Proxy`. Te mechanizmy doskonale się uzupełniają i wręcz grzechem byłoby ich nie wykorzystać razem.
I to tyle. Małym nakładem środków udało nam się odtworzyć magię PHP-a, yay!
Komentarze
Przejdź do komentarzy bezpośrednio na Githubie.
Dawne komentarze
Ten blog wcześniej korzystał z systemu komentarzy Disqus. Jednakże pożegnaliśmy się i postanowiłem, że zaimportuję do nowej wersji stare komentarze z niego. Cóż, jego system eksportu na wiele nie pozwala…
Hehe dobre... przyznam, że moją współpracę z PHP zakończyłem na wersji 5.6 już jakiś czas temu (jakoś nigdy nie mogliśmy się z PHP polubić...) ale nie miałem potrzeby używania __get, choć pamiętam, że tych magików było chyba całkiem sporo (__sleep, __wakeup, __clone jeśli dobrze pamiętam i nie mylę teraz z czymś...).
Opisałeś całkiem ciekawy problem, choć nie wiem czy w kodzie produkcyjnym byłoby to dobre... ale może to trochę już przyzwyczajenie to TypeScript i interfejsów, które załatwiają problemy nieistniejących metod i właściwości :)
Nie mniej jednak osobiście lubię takie zabawy z JS, których Ty robisz całkiem sporo na swoim blogu :)
A tak na marginesie.... to co naczelny JS'owiec polskiego internetu robi w PHP ?! (zdrajca świata front-endu :p)
> nie wiem czy w kodzie produkcyjnym byłoby to dobre
A czy ja gdzieś napisałem, że to jest dobre…? ;)
> co naczelny JS'owiec polskiego internetu robi w PHP ?!
To był język, w którym zaczynałem, zanim nie przesiadłem się na stałe do JS-a.
Gotowiec na zadanie rekturacyjne :p
heh, w sumie podobnie jak u mnie... no nie licząc czasów Turbo Pascala i świetności C++ Builder :) ale wtedy webdeweloperka była leciutko inna...
Reflect chyba jest także bardziej użyteczny, jeśli ktoś doda obiekt `{foo: undefined}` żeby sprawdzić czy obiekt posiada pole to trzeba by użyć `hasOwnProperty`, ale wtedy nie będzie obsługiwany `prototype`. A już `Foo.prototype.foo = undefined` nie ma jak sprawdzić bez `Reflect.has`.
A cóż z nieczęsto spotykanym `if (... in ...)`? Osobiście używam tej konstrukcji ponieważ jest po prostu zrozumiała i krótka w zapisie - niekoniecznie znaczy to, że jest najlepszą - coś_z_nią_nie_tak/po_prosstu_inna_"tradycyjna"_metoda/rzecz_faktycznie_mało_znana?
(Po prostu wątek który rozpocząłeś mi jakoś pasuje do tego pytania, wiec tutaj zapytuję ;x)
Zawsze myślałem że Reflect to przerost formy nad treścią ten wpis zmienił moje zdanie gdy sprawdziłem jak działa alternatywa z hasOwnProperty i sprawdzaniu `typeof target[name]` a nie pomyślałem o `name in target`. Dzięki, sprawiłeś że znowu myślę że Reflect nie ma sensu, bo można normalne w ES5 sprawdzić czy element ma właściwość.
Nie przesadzajmy, że nie ma sensu.
Po pierwsze: uważam, że jest o wiele bardziej elegancki, co czyni kod czytelniejszym.
Po drugie: `Reflect` zaczyna robić sensy, gdy połączymy go w parę z `Proxy`. Wystarczy porównać metody refleksji z pułapkami proxy: https://developer.mozilla.o... vs https://developer.mozilla.o...
Zgadza się zarówno nazwa metody, jak i przekazywane parametry. Wniosek: `Reflect` dostarcza domyślne zachowanie pułapek, które można zaimplementować w `Proxy`. Mówiąc inaczej: w końcu JS udostępnia nam domyślne zachowanie obiektów jako zestaw podręcznych funkcji. Samo w sobie może nie ma to jakiejś niesamowitej wartości, ale już przy metaprogramowaniu z wykorzystaniem `Proxy` – jak najbardziej.