Klasy

> Dodaj do ulubionych

ES6 bez tajemnic to cykl artykułów poświęconych nowym składnikom języka JavaScript, które pojawiły się w 6. edycji standardu ECMAScript, w skrócie ES6.

Tym razem odpoczniemy nieco od zaawansowanych zagadnień, którym do tej pory były poświęcone nasze artykuły. Nie będziemy omawiać żadnych niespotykanych dotychczas sposobów na pisanie kodu przy użyciu generatorów, żadnych potężnych obiektów pośredniczących, które udostępniają haki monitorujące wewnętrzne algorytmy JavaScriptu, ani żadnych nowych struktur danych, dzięki którym nie musimy opracowywać własnych rozwiązań. Zamiast tego pomówimy o prostszych sposobach na uporanie się ze starym problemem – tworzeniem konstruktora obiektów w JavaScripcie.

Problem

Powiedzmy, że chcielibyśmy przedstawić kwintesencję paradygmatu programowania obiektowego na jednym przykładzie: klasie Circle. Wyobraź sobie, że piszemy kod koła, które wykorzystamy w prostej bibliotece Canvas. Chcielibyśmy między innymi wiedzieć jak:

  • narysować koło na danej kanwie;
  • śledzić łączną liczbę narysowanych kół;
  • śledzić wartość promienia danego koła i jak wymusić na niej niezmienniki;
  • obliczyć powierzchnię danego koła.

Obecnie obowiązujące w JS niepisane zasady mówią, że najpierw należy utworzyć funkcję konstruktora, następnie dodać do niej dowolne własności, a później zamienić własność prototype konstruktora na obiekt. Taki prototypowy obiekt będzie zawierać wszystkie własności, które, na dobry początek, powinny mieć utworzone przez konstruktor instancje obiektów. Nawet jeśli zaczniemy od prostego przykładu to koniec końców otrzymamy kod, który będzie w dużej mierze szablonowy.

function Circle(radius) {
    this.radius = radius;
    Circle.circlesMade++;
}

Circle.draw = function draw(circle, canvas) { /* kod rysunku na kanwie */ }

Object.defineProperty(Circle, "circlesMade", {
    get: function() {
        return !this._count ? 0 : this._count;
    },

    set: function(val) {
        this._count = val;
    }
});

Circle.prototype = {
    area: function area() {
        return Math.pow(this.radius, 2) * Math.PI;
    }
};

Object.defineProperty(Circle.prototype, "radius", {
    get: function() {
        return this._radius;
    },

    set: function(radius) {
        if (!Number.isInteger(radius))
            throw new Error("Promień koła musi być liczbą całkowitą.");
        this._radius = radius;
    }
});

Powyższy kod jest zarówno nieporęczny jak i mało intuicyjny. By go zrozumieć, musimy dogłębnie znać działanie funkcji, a do tego wiedzieć, jak dodane własności przedostają się do utworzonych instancji obiektów. Jeśli wydaje ci się to skomplikowane, nic się nie martw. Niniejszy artykuł ma na celu pokazać o wiele prostszy sposób na pisanie kodu, który będzie robić wszystko to, o czym powiedzieliśmy.

Składnia definicji metody w JavaScripcie

By uporządkować przedstawiony sposób pisania kodu, w ES6 wprowadzono nową składnię dodawania specjalnych własności do obiektów. Choć dodanie metody area do Circle.prototype nie nastręczało trudności, to już umieszczenie metody pobierającej i ustawiającej w obiekcie radius było większym wyzwaniem. Wraz z rozwojem JS w kierunku obiektowości wzrosło zainteresowanie czytelniejszymi sposobami dodawania metod dostepowych do obiektów. Potrzebny był nowy sposób na dodawanie do obiektów metod, działający identycznie jak obj.prop = method, lecz mniej skomplikowany niż Object.defineProperty. Programistom zależało, aby z łatwością dodawać do obiektów:

  1. własności zwykłych funkcji,
  2. własności generatorów,
  3. własności zwykłych funkcji dostępowych,
  4. wszystkie wymienione własności, w taki sposób jak dodawanie własności do skończonych obiektów poprzez składnię [] – nazwiemy je obliczanymi nazwami własności.

Niektóre z tych operacji nie dało się do tej pory wykonać. Nie można było na przykład zdefiniować metody pobierającej lub ustawiającej z przypisaniem do własności obj.prop. W związku z tym potrzebna była nowa składnia. Teraz możemy natomiast pisać kod, który wygląda następująco:

var obj = {
    // Metody są teraz dodawane bez użycia słowa kluczowego,
    // nazwę funkcji stanowi nazwa własności.
    method(args) { ... },

    // Aby zamiast metody utworzyć generator, wystarczy, tak jak zwykle, dodać '*'.
    *genMethod(args) { ... },

    // Funkcje dostępowe można definiować bezpośrednio w kodzie przy pomocy słów |get| i |set|. Nie dotyczy to jednak generatorów.

    // Zauważ, że utworzona w ten sposób metoda pobierająca nie może mieć żadnych argumentów.
    get propName() { ... },

    // Zwróć również uwagę, że utworzona w ten sposób metoda ustawiająca musi mieć dokładnie jeden argument.
    set propName(arg) { ... },

    // W powyższym przykładzie zamiast nazwy można wykorzystać składnię []
 W nawiasie kwadratowym mogą znaleźć się symbole, wywołania funkcji, połączone łańcuchy
    // i każde inne wyrażenie, które odpowiada identyfikatorowi własności. Choć pokazałem
    // tę składnię na przykładzie zwykłej metody, to można z niej skorzystać także w przypadku metod dostępowych i generatorów.
    [functionThatReturnsPropertyName()] (args) { ... }
};

Możemy teraz przepisać poprzedni kod z wykorzystaniem nowej składni:

function Circle(radius) {
    this.radius = radius;
    Circle.circlesMade++;
}

Circle.draw = function draw(circle, canvas) { /* kod rysunku na kanwie */ }

Object.defineProperty(Circle, "circlesMade", {
    get: function() {
        return !this._count ? 0 : this._count;
    },

    set: function(val) {
        this._count = val;
    }
});

Circle.prototype = {
    area() {
        return Math.pow(this.radius, 2) * Math.PI;
    },

    get radius() {
        return this._radius;
    },
    set radius(radius) {
        if (!Number.isInteger(radius))
            throw new Error("Promień koła musi być liczbą całkowitą.");
        this._radius = radius;
    }
};

Jeśli mielibyśmy czepiać się szczegółów, kod ten nie jest identyczny z oryginałem. Definicje metod w literałach obiektowych są konfigurowalne i przeliczalne, zaś metody dostępowe z pierwszego przykładu wręcz przeciwnie. W praktyce jest to jednak niemalże niezauważalne, zatem by zbytnio się nie rozpisywać postanowiłem pominąć te kwestie.

Mimo wszystko zmierzamy w dobrym kierunku, prawda? Niestety, nawet mając do dyspozycji nową składnię definicji metod niewiele możemy zrobić jeśli chodzi o definicję koła, ponieważ musimy jeszcze zdefiniować funkcję Circle. Nie można bowiem dodać funkcji do własności bezpośrednio w definicji.

Składnia definicji klasy

Choć powyższe rozwiązanie było krokiem naprzód to nie zadowoliło tych, którym zależało na bardziej przejrzystym sposobie pisania kodu obiektowego w JavaScripcie. Jak argumentowali, inne języki dysponują przecież składnikiem obsługującym elementy programowania obiektowego, a mianowicie klasami.

Niech będzie – dodajmy klasy.

Chcemy stworzyć system, w którym możliwe będzie dodawanie metod do nazwanego konstrutora i do jego portotypu, dzięki czemu będą dostępne w utworzonych instancjach klasy. Skoro już mamy do dyspozycji naszą nową składnię definicji metod, to musimy z niej skorzystać. Potrzebujemy zatem jedynie znaleźć sposób na odróżnienie ogólnych elementów współdzielonych przez wszystkie instancje klasy od funkcji specyficznych dla danej instancji. W języku C++ czy Java służy do tego słowo kluczowe static. Wydaje się równie dobre jak każde inne, więc możemy go użyć.

Teraz przydałoby się jakoś oznaczyć jedną z metod jako funkcję wywoływaną w roli konstruktora. W C++ i Javie taka funkcja nosiłaby identyczną nazwę jak klasa, pozbawiona byłaby tylko typu zwrotnego. Ponieważ w JS nie ma typów zwrotnych, a i tak potrzebujemy własności .constructor, nadajmy tej metodzie nazwę constructor. Uzyskamy dzięki temu zgodność wsteczną.

Wykorzystując wszystkie omówione rozwiązania, możemy przepisać klasę Cricle, by wyglądała tak, jak powinna była wyglądać od samego początku:

class Circle {
    constructor(radius) {
        this.radius = radius;
        Circle.circlesMade++;
    };

    static draw(circle, canvas) {
        // kod rysunku na kanwie
    };

    static get circlesMade() {
        return !this._count ? 0 : this._count;
    };
    static set circlesMade(val) {
        this._count = val;
    };

    area() {
        return Math.pow(this.radius, 2) * Math.PI;
    };

    get radius() {
        return this._radius;
    };
    set radius(radius) {
        if (!Number.isInteger(radius))
            throw new Error("Promień koła musi być liczbą całkowitą.");
        this._radius = radius;
    };
}

Wow! Nie dość, że możemy pogrupować razem wszystkie fragmenty kodu związane z kołem to jeszcze całość wygląda tak… przejrzyście. Porównując to nowe rozwiązanie z kodem, od którego zaczynaliśmy. Zdecydowanie możemy mówić o postępie.

Mimo to na pewno niektórzy z was znajdą jakieś przypadki brzegowe bądź mają dodatkowe pytania. Postaram się odpowiedzieć na nie już teraz.

  • Po co te średniki? Aby „upodobnić kod do wyglądu tradycyjnych klas” postanowiliśmy zastosować tradycyjny separator. Nie podobają ci się średniki? Są opcjonalne, w kodzie tym nie jest wymagany żaden ogranicznik.
  • Co jeśli nie chcę mieć konstruktora, ale chciałbym dodawać metody do utworzonych obiektów? Nic nie szkodzi. Metoda constructor jest całkowicie opcjonalna. Jeśli jej nie dodasz, kod będzie domyślnie działał tak, jakbyś wpisał constructor() {}.
  • Czy konstruktor może być generatorem? Nie. Dodanie konstruktora niebędącego zwykłą metodą spowoduje błąd typu. Dotyczy to zarówno generatorów jak i metod dostępowych.
  • Czy mogę zdefiniować konstruktor o obliczanej nazwie własności? Niestety nie. Byłoby go trudno wykryć, więc nie będziemy z tym eksperymentować. Możliwe jest zdefiniowanie metody o obliczanej nazwie constructor – nie będzie ona jednak wtedy funkcją konstruktora klasy.
  • Co jeśli zmienię definicję klasy Circle? Czy spowoduje to nieprawidłowe działanie instancji new Circle? Nie. Klasy, podobnie jak wyrażenia funkcyjne, są wewnętrznie związane ze swoją nazwą. Wiązania tego nie można zmiennić z zewnątrz, zatem niezależnie od wartości, jaką przypiszemy zmiennej Circle w wyższym zakresie, zawarta w konstruktorze inkrementacja Circle.circlesMade++ będzie funkcjonować poprawnie.
  • Dobrze, ale mógłbym przekazać literał obiektowy bezpośrednio jako argument funkcji. Wydaje się, że wtedy te nowe klasy nie będą działać. Na szczęście w ES6 zostały dodane także wyrażenia klasowe! Mogą być nazwane lub nienazwane. Zachowują się identycznie jak opisane powyżej klasy, z jednym wyjątkiem – nie tworzą zmiennych w zakresie, w którym zostały zadeklarowane.
  • A co z tymi sztuczkami z przeliczalnością itd.? Programistom zależało na przeliczalności, by można było umieszczać metody w obiektach, ale kiedy próbowano wyliczyć własności takiego obiektu, to zwracane były jedynie własności danych. Ma to sens. Z tego powodu metody umieszczane w klasach są konfigurowalne, lecz nie przeliczalne.
  • Zaraz, zaraz… A gdzie się podziały zmienne wystąpień? Co ze stałymi statycznymi? Tu mnie masz. Na tę chwilę nie są one dostępne w definicjach klas ES6. Jest jednak dobra wiadomość! W gronie pracujących nad specyfikacją języka są zwolennicy udostępnienia możliwości umieszczania wartości static i const w składni klas, w tym ja. Poruszaliśmy już nawet ten temat podczas naszych spotkań. Myślę, że niedługo czeka nas większa debata na ten temat.
  • OK, mimo wszystko to fajna rzecz! Czy mogę już korzystać z tych klas? – Niezupełnie. Możesz jednak spróbować, korzystając z wypełniaczy (zwłaszcza z wtyczki Babel). Niestety minie jeszcze trochę czasu zanim klasy ES6 będą standardowo obsługiwane przez wszystkie główne przeglądarki Wszystkie omówione dziś składniki zaimplementowałem w Firefoksie Nightly. Są one również zaimplementowane w przeglądarkach Edge i Chrome – trzeba je jednak włączyć samodzielnie. Niestety, aktualnie nie ma chyba żadnej implementacji klas ES6 w Safari.
  • Java i C++ zawierają podklasy oraz słowo kluczowe super. Czy są one dostępne w JS? W tym artykule nic na ten temat nie ma. Owszem, są! To jednak materiał na osobny wpis. Odwiedzaj nas regularnie, by nie przegapić artykułu o podklasach, w którym dogłębniej omówimy możliwości klas w JavaScripcie.

W tym miejscu chciałbym podziękować Jasonowi Orendorffowi i Jeffowi Waldenowi – bez ich wskazówek i recenzji kodu nie zdołałbym zaimplementować klas ES6.

W następnym artykule Jason Orendorff omówi funkcjonowanie słów kluczowych let i const.

Autor: Eric Faust

Źródło: https://hacks.mozilla.org/2015/07/es6-in-depth-classes/

Tłumaczenie: Joanna Liana

Treść tej strony jest dostępna na zasadach licencji CC BY-SA 3.0