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!