ECMAScript 5: Object.create() i prototypy

Poniższa notka to obiecana trzecia część cyklu o nowym podejściu do obiektów w ES5, poświęcona tworzeniu obiektów. Od poprzednich dwóch części minęło trochę czasu, ale przecież lepiej późno niż wcale. Zaczynamy.

W dwóch poprzednich odcinkach opisałem, w jaki sposób można ograniczać możliwości modyfikacji obiektów i jak tworzyć własności obiektów przy użyciu deskryptorów własności. W pierwszym z artykułów wspomniałem też o tym, że każdy obiekt w JS znajduje się w łańcuchu prototypów, po których dziedziczy własności, których sam nie posiada.

Krótko przypomnijmy najważniejsze rzeczy. ECMAScript 3 nie posiadał możliwości bezpośredniego zarządzania prototypami obiektów. Ustawianie prototypu danego obiektu możliwe było tylko przez ustawienie własności prototype funkcji, a następnie wywołanie jej jako konstruktora przy użyciu operatora new:

function MyObj() {
    /* treść konstruktora; może być pusta */
}

MyObj.prototype = { // prototyp dla myObj
    prop1: "value1"
};

var myObj = new MyObj();

Tak więc zawsze konieczny był konstruktor, nawet jeśli miałby zupełnie nic nie robić. Było to dość absurdalne, jako że JS – język o prototypowej formie dziedziczenia – umożliwiał zarządzanie prototypami wyłącznie poprzez notację zaprojektowaną od początku jako coś, co miało – lepiej lub gorzej – udawać klasową formę dziedziczenia (pomijam tu rozszerzenia niestandardowe, takie jak dostępna m. in. w Gecko własność __proto__).

Douglas Crockford zaproponował swego czasu funkcję, która tworzyła tymczasowy, pusty konstruktor, ustawiała jego własność prototype na zadany obiekt i zwracała nowy obiekt utworzony operatorem new:

Object.beget = function (o) {
    function F() {}
    F.prototype = o;
    return new F();
};

Używało się tego w sposób następujący:

var myProto = {
    prop1: "value1",
    method1: function () { /* ... */ }
};

var myObj = Object.beget(myProto);

Funkcja beget Crockforda stała się punktem wyjścia do dalszych prac w TC-39. W efekcie w piątej edycji ECMAScriptu pojawiła się funkcja Object.create(), która działa bardzo podobnie:

var myProto = {
    prop1: "value1",
    method1: function () { /* ... */ }
};

var myObj = Object.create(myProto);

Mamy tu jednak dwie ważne różnice. Po pierwsze, create jako funkcja wbudowana w silnik JS nie tworzy żadnych tymczasowych funkcji, ale operuje bezpośrednio na wewnętrznej własności [[Prototype]]. Po drugie, oprócz pierwszego argumentu, jakim jest obiekt, który ma być wykorzystany jako prototyp, Object.create może także przyjąć opcjonalny argument drugi, którym jest hash deskryptorów własności – dokładnie to samo, co jest argumentem Object.defineProperties():

var myProto = {
    prop1: "value1",
    method1: function () { /* ... */ }
};

var propDescriptors = {
    prop2: {
        writable:     false,
        enumerable:   false,
        configurable: false,
        value: 7
    }
    /* i tu ew. dalsze deskryptory */
}

var myObj = Object.create(myProto, propDescriptors);

Gdzie to działa? Object.create() znajdziecie w silniku Gecko od wersji 2.0 (Firefox 4 i nowsze), w IE 9 (w IE 9 standards mode) i w nowszych, w Safari od wersji 5 oraz w Chrome 5 i nowszych. Opera jak dotąd nie doczekała się implementacji. Pozostaje stosowanie tzw. polyfills. Niestety, Object.create() w pełni działającej wersji (z obsługą deskryptorów) nie może być niestety dodana jako polyfill dla przeglądarek nieobsługujących ES5.

Jeśli jednak nie zamierzamy korzystać z deskryptorów, Object.beget() Crockforda po zmianie nazwy jest wciąż dość dobrym zamiennikiem. Takie rozwiązanie proponuje np. Mozilla Developer Network w artykule o Object.create().

ES5 dostarcza także funkcji pozwalającej odnaleźć prototyp danego obiektu: Object.getPrototypeOf(obj):

var myProto = {
    prop1: "value1",
    method1: function () { /* ... */ }
};

var myObj = Object.create(myProto);

alert(Object.getPrototypeOf(myObj) === myProto);
// wyświetla: true

W dalszym ciągu standard ECMAScript nie pozwala jednak na podmianę prototypu już istniejącego obiektu.

Wsparcie przeglądarek wygląda tak samo, jak dla create(). Niestety, nie ma możliwości stworzenia działającego wszędzie polyfilla, ale warto pamiętać, że wiele przeglądarek (w tym Opera) obsługuje niestandardową własność __proto__ (w takich przeglądarkach: myProto === myObj.__proto__).

Na koniec kilka słów o tym, czy nie da się tego wszystkiego zrobić prościej. Miałem już kilka prezentacji na ten ES5, a także dyskusji z innymi programistami (w tym m. in. z Damianem Wielgosikiem) i bardzo często stykałem się z pytaniem o to, czy podoba mi się taka składnia. Otóż nie, nie podoba mi się, ale w tej chwili nie mamy niestety innego wyjścia.

Cała nadzieja w ECMAScripcie 6: prawdopodobnie notacja literału obiektowego zostanie tak rozszerzona, by nie trzeba było korzystać z funkcji takich jak create() do określenia prototypu. Być może więc w celu utworzenia obiektu myObj dziedziczącego po myProto, z własną, niezapisywalną (:=), niewyliczalną (~) i niekonfigurowalną (!) własnością prop2 (takiego jak dwa przykłady temu) będzie można napisać po prostu tak:

let myObj = myProto <| {
    ~!prop2 := 7
};

Czy tak się stanie, dopiero się przekonamy. Czy jest to bardziej czytelne niż create() i deskryptory? Sami zdecydujcie. Na pewno tak byłoby jednak sporo krócej. :)

5 thoughts on “ECMAScript 5: Object.create() i prototypy

  1. Jeśli chodzi o __proto__ to jest dostępny na pewno też w V8, więc Chrome, Node i v8cgi mają tę właściwość dostępną. Nie wiem jak Safari i IE9, ale stała się praktycznie standardem. Sam nie wiem tylko czy się z tego cieszę :) Bo jest pomocna, ale… “niesmak pozostał” :P

    Jeśli chodzi zaś o Operę, to fakt, że są oni kompletnie do tyłu z implementacją ES5 (tak naprawdę praktycznie nic nie zaczęli jeszcze) powoduje, że dla mnie jako programisty jest to w tej chwili złom. Mam nadzieję, że wezmą się wreszcie do roboty.

    Wracając do tematu – Object.create/beget chyba nigdy mi się jeszcze nie przydał, jako że zawsze coś w tym konstruktorze się znajdzie. Choćby inicjalizacja właściwości złożonych, bo ich zdefiniowanie tylko w prototypie kończy się referencją wszystkich obiektów do tego samej właściwości, co generuje fajne bugi :)

    Deskryptory… Wrrr :) Ohydztwo. Ktoś z “JavaScript” wyciął “Script” i tak powstał Chocapic! Znaczy się deskryptory. Wszystko będzie lepsze od nich. Choć same !~:= to też nie ideał, bo kompletnie nie są “self-explanatory”, ale wolę je :) A najbardziej wolałbym krótkie słówka kluczowe, ale pewnie wtedy CoffeScript wprowadziłby krótsze formy i jego zwolennicy mieliby kolejny kiepski argument.

      1. Że zaczęli wiem, ale jak widać w tabeli spadli za IE9 :) Nie tego bym się spodziewał po tak dobrej przeglądarce jaką kiedyś była Opera. W tej chwili za dużo bajerów w GUI, za mało w kodzie. Choć oczywiście moje spojrzenie, jest spojrzeniem programisty. Zwykły użytkownik pewnie odbiera to inaczej (dopóki działa mu Gmail ;>).

        Dzięki za kangaxa – zapomniałem o tych tabelach.

  2. Dobry post.

    Czy myślisz, że w przyszłych wersjach pozwoli się na manipulację własności [[Prototype]] bieżących obiektów? __proto__ daje taką możliwość, ale jak wspomniałeś, nie jest w standardzie.

    W porównaniu do klasycznych systemów dziedziczenia opartych na statycznych klasach, delegacja do prototypów wydaje się być lżejszym i bardziej elastycznym sposobem dziedziczenia. Ale dopiero możliwość zmiany prototypu w locie (podobnie jak w przypadku dynamicznych klas, np. w Pythonie: a = A(); a.__class__ = B) dopełnia w moim przekonaniu tę elastyczność.

Leave a comment