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. :)
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.
Safari ma __proto__, IE – nie.
Opera owszem, jest w tyle, ale nie jest tak, że w ogóle nie zaczęli. Patrz tu: http://kangax.github.com/es5-compat-table/
Ż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.
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ść.
Kiedy ostatnio czytałem posty na es-discuss na ten temat, mowa była o tym, że możliwość modyfikacji prototypów w locie jest niepożądana i utrudnia optymalizację. Tak więc TC39 nie zamierza tego standaryzować, a Brendan sugeruje powolne porzucenie edytowalnego __proto__ w dalszej przyszłości.
Dyskusja (z marca) jest w tym wątku: https://mail.mozilla.org/pipermail/es-discuss/2011-March/thread.html#13131