Przewiń do treści 

Uniwersalny getter

Opublikowany:
Autor:
Comandeer
Kategorie:
JavaScript

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…

  1. Opublikowany:
    Autor:
    tomasz_sochacki

    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)

    1. Opublikowany:
      Autor:
      Comandeer

      > 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.

      1. Opublikowany:
        Autor:
        tomasz_sochacki
        A czy ja gdzieś napisałem, że to jest dobre…? ;)


        Gotowiec na zadanie rekturacyjne :p

        To był język, w którym zaczynałem, zanim nie przesiadłem się na stałe do JS-a.


        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...

  2. Opublikowany:
    Autor:
    jcubic

    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`.

    1. Opublikowany:
      Autor:
      Evolveye

      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)

      1. Opublikowany:
        Autor:
        jcubic

        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ść.

        1. Opublikowany:
          Autor:
          Comandeer

          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.