Dekoratory JavaScript: What They Are and When to Use Them
On 26 stycznia, 2022 by adminWraz z wprowadzeniem ES2015+, oraz w miarę jak transpilacja stała się powszechna, wielu z Was zetknęło się z nowszymi funkcjami języka, czy to w prawdziwym kodzie, czy w tutorialach. Jedną z tych funkcji, która często sprawia, że ludzie drapią się po głowie, gdy po raz pierwszy się z nią zetkną, są dekoratory JavaScript.
Dekoratory stały się popularne dzięki ich zastosowaniu w Angular 2+. W Angular, dekoratory są dostępne dzięki TypeScript, ale w JavaScript są one obecnie propozycją etapu 2, co oznacza, że powinny być częścią przyszłej aktualizacji języka. Przyjrzyjmy się, czym są dekoratory i jak można ich używać, aby kod był czystszy i bardziej zrozumiały.
Co to jest dekorator?
W swojej najprostszej formie, dekorator jest po prostu sposobem na owinięcie jednego fragmentu kodu innym – dosłownie „dekorując” go. Jest to koncepcja, o której być może słyszałeś wcześniej jako o kompozycji funkcjonalnej lub funkcjach wyższego rzędu.
Jest to już możliwe w standardowym JavaScripcie dla wielu przypadków użycia, po prostu przez wywołanie jednej funkcji w celu zawinięcia innej:
function doSomething(name) { console.log('Hello, ' + name);}function loggingDecorator(wrapped) { return function() { console.log('Starting'); const result = wrapped.apply(this, arguments); console.log('Finished'); return result; }}const wrapped = loggingDecorator(doSomething);
Ten przykład tworzy nową funkcję – w zmiennej wrapped
– która może być wywołana dokładnie w taki sam sposób jak funkcja doSomething
i zrobi dokładnie to samo. Różnica polega na tym, że wykona ona pewne logowanie przed i po wywołaniu zawiniętej funkcji:
doSomething('Graham');// Hello, Grahamwrapped('Graham');// Starting// Hello, Graham// Finished
How to Use JavaScript Decorators
Dekoratory używają specjalnej składni w JavaScript, dzięki której są poprzedzone symbolem @
i umieszczone bezpośrednio przed dekorowanym kodem.
Uwaga: w momencie pisania, dekoratory są obecnie w formie „Stage 2 Draft”, co oznacza, że są w większości skończone, ale wciąż podlegają zmianom.
Możliwe jest użycie tak wielu dekoratorów na tym samym kawałku kodu, jak tylko chcesz, a zostaną one zastosowane w kolejności, w jakiej je zadeklarujesz.
Na przykład:
@log()@immutable()class Example { @time('demo') doSomething() { // }}
To definiuje klasę i stosuje trzy dekoratory – dwa do samej klasy, a jeden do właściwości klasy:
-
@log
może rejestrować cały dostęp do klasy -
@immutable
może uczynić klasę niezmienną – być może wywołujeObject.freeze
na nowych instancjach -
@time
zarejestruje, jak długo trwa wykonanie metody i wyloguje to za pomocą unikalnego znacznika.
Obecnie używanie dekoratorów wymaga wsparcia transpilatora, ponieważ żadna obecna przeglądarka lub wydanie Node nie ma jeszcze dla nich wsparcia. Jeśli używasz Babel, jest to możliwe po prostu przez użycie wtyczki transform-decorators-legacy.
Uwaga: użycie słowa „legacy” w tej wtyczce jest spowodowane tym, że wspiera ona sposób obsługi dekoratorów w Babel 5, który może się różnić od ostatecznej formy, gdy zostaną one znormalizowane.
Dlaczego używać dekoratorów?
Podczas gdy funkcjonalna kompozycja jest już możliwa w JavaScript, jest znacznie trudniej – lub nawet niemożliwe – zastosować te same techniki do innych fragmentów kodu (np. klas i właściwości klas).
Propozycja dekoratora dodaje wsparcie dla dekoratorów klas i właściwości, które mogą być użyte do rozwiązania tych problemów, a przyszłe wersje JavaScript prawdopodobnie dodadzą wsparcie dla dekoratorów dla innych kłopotliwych obszarów kodu.
Dekoratory pozwalają również na czystszą składnię do stosowania tych wrapperów wokół kodu, w wyniku czego powstaje coś, co w mniejszym stopniu odciąga od rzeczywistej intencji tego, co piszesz.
Różne typy dekoratorów
Obecnie, jedyne typy dekoratorów, które są obsługiwane, dotyczą klas i członków klas. Obejmuje to właściwości, metody, gettery i settery.
Dekoratory są właściwie niczym więcej niż funkcjami, które zwracają inną funkcję, i które są wywoływane z odpowiednimi szczegółami dekorowanego elementu. Te funkcje dekoratorów są obliczane raz, gdy program jest uruchamiany po raz pierwszy, a dekorowany kod jest zastępowany wartością zwracaną.
Dekoratory członków klasy
Dekoratory właściwości są stosowane do pojedynczego członka klasy – niezależnie od tego, czy są to właściwości, metody, gettery czy settery. Ta funkcja dekoratora jest wywoływana z trzema parametrami:
-
target
: klasa, w której znajduje się członek. -
name
: nazwa członka w klasie. -
descriptor
: deskryptor członka. Jest to w zasadzie obiekt, który zostałby przekazany do Object.defineProperty.
Klasycznym przykładem używanym tutaj jest @readonly
. Jest to zaimplementowane tak prosto jak:
function readonly(target, name, descriptor) { descriptor.writable = false; return descriptor;}
Dosłownie aktualizując deskryptor właściwości, aby ustawić flagę „writable” na false.
Jest to następnie używane na właściwości klasy w następujący sposób:
class Example { a() {} @readonly b() {}}const e = new Example();e.a = 1;e.b = 2;// TypeError: Cannot assign to read only property 'b' of object '#<Example>'
Ale możemy zrobić coś lepszego niż to. Możemy faktycznie zastąpić udekorowaną funkcję innym zachowaniem. Na przykład, zalogujmy wszystkie dane wejściowe i wyjściowe:
function log(target, name, descriptor) { const original = descriptor.value; if (typeof original === 'function') { descriptor.value = function(...args) { console.log(`Arguments: ${args}`); try { const result = original.apply(this, args); console.log(`Result: ${result}`); return result; } catch (e) { console.log(`Error: ${e}`); throw e; } } } return descriptor;}
To zastępuje całą metodę nową, która rejestruje argumenty, wywołuje oryginalną metodę, a następnie rejestruje dane wyjściowe.
Zauważ, że użyliśmy tutaj operatora spread, aby automatycznie zbudować tablicę ze wszystkich dostarczonych argumentów, co jest bardziej nowoczesną alternatywą dla starej wartości arguments
.
Możemy zobaczyć to w użyciu w następujący sposób:
class Example { @log sum(a, b) { return a + b; }}const e = new Example();e.sum(1, 2);// Arguments: 1,2// Result: 3
Zauważysz, że musieliśmy użyć nieco zabawnej składni, aby wykonać ozdobioną metodę. Mogłoby to zająć cały artykuł, ale w skrócie, funkcja apply
pozwala na wywołanie funkcji, określając wartość this
i argumenty, z którymi ma zostać wywołana.
Podnosząc to do góry, możemy sprawić, że nasz dekorator przyjmie jakieś argumenty. Na przykład, przepiszmy nasz dekorator log
w następujący sposób:
function log(name) { return function decorator(t, n, descriptor) { const original = descriptor.value; if (typeof original === 'function') { descriptor.value = function(...args) { console.log(`Arguments for ${name}: ${args}`); try { const result = original.apply(this, args); console.log(`Result from ${name}: ${result}`); return result; } catch (e) { console.log(`Error from ${name}: ${e}`); throw e; } } } return descriptor; };}
Teraz robi się to bardziej skomplikowane, ale kiedy to rozłożymy, mamy to:
- Funkcja,
log
, która przyjmuje jeden parametr:name
. - Funkcja ta zwraca następnie funkcję, która sama jest dekoratorem.
Jest ona identyczna z wcześniejszym dekoratorem log
, z tą różnicą, że wykorzystuje parametr name
z funkcji zewnętrznej.
Jest ona następnie używana w następujący sposób:
class Example { @log('some tag') sum(a, b) { return a + b; }}const e = new Example();e.sum(1, 2);// Arguments for some tag: 1,2// Result from some tag: 3
Od razu widać, że pozwala nam to rozróżniać różne linie dziennika za pomocą znacznika, który sami dostarczyliśmy.
To działa, ponieważ wywołanie funkcji log('some tag')
jest od razu oceniane przez runtime JavaScript, a następnie odpowiedź z tego jest używana jako dekorator dla metody sum
.
Dekoratory klas
Dekoratory klas są stosowane do całej definicji klasy za jednym zamachem. Funkcja dekoratora jest wywoływana z pojedynczym parametrem, który jest funkcją konstruktora, która jest dekorowana.
Zauważ, że jest to stosowane do funkcji konstruktora, a nie do każdej instancji klasy, która jest tworzona. Oznacza to, że jeśli chcesz manipulować instancjami, musisz zrobić to sam, zwracając zawiniętą wersję konstruktora.
Ogólnie rzecz biorąc, są one mniej przydatne niż dekoratory członków klasy, ponieważ wszystko, co możesz zrobić tutaj, możesz zrobić za pomocą prostego wywołania funkcji w dokładnie taki sam sposób. Wszystko, co zrobisz za ich pomocą, musi zakończyć się zwróceniem nowej funkcji konstruktora, która zastąpi konstruktor klasy.
Powracając do naszego przykładu logowania, napiszmy taki, który loguje parametry konstruktora:
function log(Class) { return (...args) => { console.log(args); return new Class(...args); };}
Tutaj akceptujemy klasę jako nasz argument i zwracamy nową funkcję, która będzie działać jako konstruktor. To po prostu wylogowuje argumenty i zwraca nową instancję klasy skonstruowaną z tych argumentów.
Na przykład:
@logclass Example { constructor(name, age) { }}const e = new Example('Graham', 34);// console.log(e);// Example {}
Widzimy, że skonstruowanie naszej klasy Example wyloguje dostarczone argumenty i że skonstruowana wartość jest rzeczywiście instancją Example
. Dokładnie to, czego chcieliśmy.
Przekazywanie parametrów do dekoratorów klas działa dokładnie tak samo jak w przypadku członków klasy:
function log(name) { return function decorator(Class) { return (...args) => { console.log(`Arguments for ${name}: args`); return new Class(...args); }; }}@log('Demo')class Example { constructor(name, age) {}}const e = new Example('Graham', 34);// Arguments for Demo: argsconsole.log(e);// Example {}
Przykłady z prawdziwego świata
Dekoratory Core
Istnieje fantastyczna biblioteka zwana Core Decorators, która dostarcza kilka bardzo użytecznych wspólnych dekoratorów, które są gotowe do użycia już teraz. Generalnie pozwalają one na bardzo użyteczną wspólną funkcjonalność (np. czas wywoływania metod, ostrzeżenia o deprecjacji, zapewnienie, że wartość jest tylko do odczytu), ale wykorzystując znacznie czystszą składnię dekoratora.
React
Biblioteka React bardzo dobrze wykorzystuje koncepcję komponentów wyższego rzędu. Są to po prostu komponenty React, które są napisane jako funkcja, i które owijają się wokół innego komponentu.
Kup nasz kurs Premium: React The ES6 Way
Są one idealnym kandydatem do wykorzystania jako dekorator, ponieważ bardzo niewiele trzeba zmienić, aby to zrobić. Na przykład biblioteka react-redux ma funkcję connect
, która jest używana do łączenia komponentu React ze sklepem Redux.
Ogólnie rzecz biorąc, można by jej użyć w następujący sposób:
class MyReactComponent extends React.Component {}export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);
Jednakże, ze względu na to, jak działa składnia dekoratorów, można to zastąpić następującym kodem, aby osiągnąć dokładnie tę samą funkcjonalność:
@connect(mapStateToProps, mapDispatchToProps)export default class MyReactComponent extends React.Component {}
MobX
Biblioteka MobX szeroko wykorzystuje dekoratory, umożliwiając łatwe oznaczanie pól jako Observable lub Computed oraz oznaczanie klas jako Obserwatorów.
Podsumowanie
Dekoratory członków klas zapewniają bardzo dobry sposób na zawijanie kodu wewnątrz klasy w bardzo podobny sposób do tego, w jaki można to już robić dla funkcji wolnostojących. Zapewnia to dobry sposób na napisanie prostego kodu pomocniczego, który może być zastosowany w wielu miejscach w bardzo czysty i łatwy do zrozumienia sposób.
Jedynym ograniczeniem w użyciu takiego udogodnienia jest twoja wyobraźnia!
Dodaj komentarz