Wpis z mikrobloga

Ponieważ poprzednie JSowe wypociny zostały tutaj przyjęte całkiem ciepło, jedziemy z następnym. Dzisiaj porozmawiamy sobie o hermetyzacji, przestudiujemy dokładnie jak działają domknięcia, liźniemy trochę gettery/settery no i będzie na koniec odrobinę ES6.

Wybaczcie, że przykłady nie będą inline'owe, ale już cholery dostawałem od tego, że mi się whitespace'y kasowały - nawet mimo tego, że używałem zamiast nich alt+255 - tragedia z tym wypokowym markdownem :S

Hermetyzacja w JavaScript

JavaScript jest po trochu językiem obiektowym, funkcyjnym i proceduralnym, co skutkuje tym, że nie jest ani tym, ani tym, ani tym. Dlatego jego obiektowość zawiera kilka rozwiązań które są co najmniej niewygodne. Jednym z nich jest hermetyzacja, a raczej jej brak.

Każdy obiekt to po prostu hashmapa, mapująca string na dowolny JSowy typ - to wiemy wszyscy. Wszystkie własności obiektów są jednak publiczne, dostępne dla każdego - nie ma ani modyfikatorów dostępu do pól, ani interfejsów mówiących jakie pola dany obiekt eksponuje na zewnątrz (ubolewam zwłaszcza nad brakiem interfejsów, ponieważ regularnie wykorzystywanym wzorcem jest przekazywanie do funkcji literałów obiektów zawierających konfigurację - interfejsy bardzo by tutaj pomogły). Mądrzy ludzie postanowili jednak "coś z tym zrobić" i oto szybko znaleziono sposób na osiągnięcie enkapsulacji - domknięcia.

Domknięcia

Domknięcie, aka closure, to rzecz wynikająca z tego że funkcje są obiektami pierwszej klasy - powstaje wówczas coś, co się nazywa Funarg Problem*. Aby go rozwiązać zastosowano właśnie domknięcia. Zajrzyjmy głęboko w czeluści JSu i dowiedzmy się jak one działają.

W skrócie rzecz ujmując, w JS możemy deklarować funkcje wewnątrz funkcji, co w porównaniu ze statycznym rozwiązywaniem nazw zmusza aby rekordy aktywacji ("ramki stosu") nie były trzymane na stosie, a zamiast tego w pewnych powiązanych ze sobą obiektach, tworzących drzewo rekordów aktywacji. Za każdym razem, kiedy deklarujemy funkcję dostaje ona wewnętrzną własność

[[Scope]]
, która będzie wykorzystywana przy rozwiązywaniu nazw zmiennych. Możemy o tej własności myśleć jako o zbiorze wszystkich widocznych w momencie deklaracji

environment records
czyli, w skrócie mówiąc, obiektów trzymających w sobie odwzorowanie nazwa zmiennej -> wartość. To, co znajduje się we własności

[[Scope]]
da się opisać dwiema prostymi regułkami:

1. funkcje deklarowane w przestrzeni globalnej lub przy użyciu konstruktora

new Function()
mają w swoim

[[Scope]]
wyłącznie globalny

environment record
2. funkcje deklarowane wewnątrz innych funkcji jako

[[Scope]]
otrzymują aktualnie wykonywany kontekst.

Aktualnie wykonywany kontekst to nic innego, jak zmienne lokalne danej funkcji (lokalny environment record), który posiada referencję na

[[Scope]]
funkcji wywołanej.

Nic lepiej nie powie niż przykłady:

http://ideone.com/tpGvYo

W momencie deklaracji funkcji A dostała ona własność

[[Scope]]
wskazującą na kontekst globalny:

A.[[Scope]] = { x: 1, A: [Object Function] }
W momencie wywołania tej funkcji, tworzony jest nowy kontekst wykonania zawierający zmienne lokalne + referencję na

[[Scope]]
funkcji

A
(zgodnie z punktem 2 z wcześniej). Wyobraźmy sobie go jako coś takiego (strzałka oznacza że "zawiera referencję na"):

{ y: 2 } -> A.[[Scope]]
ponieważ

A.[[Scope]]
to kontekst globalny, to cały łańcuch zasięgów dla funkcji

A
prezentuje się następująco:

{ y: 2 } -> { x: 1, A: [Object Function] }
Drugi przykład:

http://ideone.com/6xMVEL

Funkcja

A
ma

[[Scope]]
wskazujący na kontekst globalny. Gdy ją wywołamy tworzymy jej kontekst wywołania, który zawiera lokalne zmienne (zmienną

y
oraz funkcję

B
) + referencję na

[[Scope]]
funkcji

A
. Czyli wygląda tak:

{ y: 2, B: [Object Function] } -> { x: 1, A: [Object Function] }
Jest to jednocześnie własność

[[Scope]]
funkcji

B
, jako że ta jest tutaj deklarowana (punkt 2 regułek o

[[Scope]]
). Ponieważ dalej wywołujemy funkcję

B
, tworzymy kolejny kontekst wywołania. Czyli zgodnie z punktem 2:

{ z: 3 } -> B.[[Scope]]
rozwijając to dalej mamy

{ z: 3 } -> { y: 2, B: [Object Function] } -> A.[[Scope]]
i jeszcze dalej

{ z: 3 } -> { y: 2, B: [Object Function] } -> { x: 1, A: [Object Function] }
są to wszystkie zmienne widoczne wewnątrz funkcji

B
.

Tyle z praktyki, przejdźmy do tego jak

[[Scope]]
pozwala tworzyć domknięcia.

http://ideone.com/8ttCx1

na chwilę obecną

[[Scope]]
funkcji A to

{ x: 1, A: [Object Function], a: undefined }
teraz wywołajmy funkcję A:

var a = A();
Wywołując tę funkcję tworzymy nowy kontekst, który zawiera:

{ y: 1, B: [Object Function] } -> A.[[Scope]]
czyli

{ y: 1, B: [Object Function] } -> { x: 1, A: [Object Function], a: [Object Function] }
zwróćmy uwagę że

B
w pierwszym kontekście i

a
w kontekście globalnym wskazują na tę samą funkcję (ten sam obiekt w pamięci). Funkcja

A
kończy działanie, ale funkcja

B
zostaje zwrócona. I nadal ma dostęp do swojego

Scope
'a! Czyli mimo że funkcja

A
skończyła działanie to

B
nadal ma dostęp do zmiennej

y
- zmiennej, która była zmienną lokalną zakończonej już funkcji - a to dlatego, że w swoim

[[Scope]]
zawiera odpowiednie elementy. Istotnie:

http://ideone.com/zl76o9

Co więcej kolejne wywołanie funkcji A zwróci inną funkcję B, zawierającą inny kontekst wywołania

var b = A();

b(); //konsola: 1

a(); //konsola: 4

Porównajmy:

a.[[Scope]] == { y: 4, B: [Object Function] } -> { x: 1, A: [Object Function], a: [Object Function] }

b.[[Scope]] == { y: 2, B: [Object Function] } -> { x: 1, A: [Object Function], a: [Object Function] }

Pierwsze obiekty są różne, bo to były dwa różne konteksty wykonania. Drugi natomiast to ten sam obiekt.

Czy już wiecie do czego to można wykorzystać?

Prywatność z użyciem domknięć

Do zmiennej

y
z poprzedniego przykładu nie można się dostać w żaden sposób z zewnątrz - można to robić jedynie w ciałach funkcji zadeklarowanych wewnątrz funkcji

A
oraz w samej funkcji

A
. Nic nie stoi jednak na przeszkodzie, żeby takie funkcje zwrócić i to ich użyć do manipulowania zmiennymi lokalnymi, niedostępnymi z zewnątrz. To jest właśnie pomysł na hermetyzację: zadeklarować obiekt w kontekście lokalnym jakiejś funkcji, jego "prywatne" składowe zasymulować zmiennymi lokalnymi tej samej funkcji, a następnie zwrócić obiekt na zewnątrz. Zobaczmy przykład:

http://ideone.com/l6Jf8R

W linijce pierwszej widzimy, że obiekt nie ma takiej własności jak

name
. Istotnie, nie ma, ponieważ my do zasymulowania składowej prywatnej używamy lokalnych zmiennych funkcji o uroczej nazwie

ble
. Funkcja dostępna pod

getName
jednakże w momencie swojej deklaracji widziała zmienną

name
- jej

[[Scope]]
przedstawia się tak:

{ obj: [Object Object], name: "bar" } -> { foo: [Object Object] }
co sprawia, że wewnątrz siebie może się odwoływać do zmiennej

name
. Analogicznie jest z

setName
.

Uwaga! Pamiętajmy że w JS prymitywy są przekazywane przez wartość, ale już obiekty przez referencję. Oznacza to, że jeśli zmienna name byłaby obiektem, to użycie getName dałoby nam bezpośredni dostęp do tego obiektu, a więc utracilibyśmy swoją hermetyzację!


Wady tego podejścia

Wszystko super, wow, hermetyzacja bardzo, ojeju! Normalnie JavaScript nagle stał się 3 tysiące razy lepszym językiem, aż mam ochotę w nim pisać, prawda? No bo to podejście nie ma wad, c'nie? No bo chyba mi nie powiecie, że ma, co?

Owszem, ma. Niestety.

Dopóki korzystamy z singletonów (aka modułów) to wszystko jest okej. Problem pojawia się, kiedy chcemy bawić się w "klasy".

http://ideone.com/TrAwvA

Czy już widzicie w czem rzecz? Ano w marnowaniu pamięci - każda instancja "klasy"

Person
będzie mieć swoją własną, praktycznie identyczną funkcję

getName
i

setName
. Wiemy, że w takim wypadku najlepiej byłoby te metody wynieść do prototypu - problem jest jednak taki, że przeniesienie ich do prototypu blokuje możliwość użycia domknięć, a więc nie da się w JS zrobić naraz i hermetyzacji domknięciami, i przenieść funkcji operującej na tak zasymulowanych własnościach prywatnych do prototypu.

Ma to też inną wadę - strasznie upierdliwe debuggowanie. Robiąc np.

console.dir
nie zobaczymy naszych składowych prywatnych, bo one będą siedzieć w domknięciach. Środowiska takie jak Firebug pozwalają dobrać się do zmiennych z domknięć, aby dobrać się do zmiennej

name
z poprzedniego przykładu piszemy

me.getName.%name
- wyświetli to nam zmienną

name
z kontekstu otaczającego funkcję

me.getName
czyli dokładnie to co chcemy uzyskać, ale nie jest to wcale wygodne.

Czy możemy coś z tym zrobić? Jasne, kombinować zawsze można. Zastanówmy się jednak wpierw, jak chcemy egzekwować "prywatność"

Dostęp wyłącznie dla uprzywilejowanych

Przykładowo możemy chcieć, żeby metody były wołane tylko w kontekście obiektu będącego instancją naszej "klasy"- nic prostszego, na górze każdej prywatnej metody dodajemy

if (!(this instanceof Person)) {

return;

}

ewentualnie możemy zamienić warunek na

if(Object.getPrototypeOf(this) !== Person.prototype)
, co wymusi bezpośrednie dziedziczenie. Żeby się nie powtarzać możemy zamiast returna rzucić wyjątek i wydzielić to do metody np.

throwIfNoPersmission
, a potem na górze prywatnych metod dać

throwIfNoPermission(this)
- oczywiście wewnątrz throwIfNoPermission nie porównywać

this
, tylko obiektu przekazanego w argumencie.

Jeśli chodzi o własności to mamy w sumie problem. Moglibyśmy skorzystać z getterów i setterów, ale są one niestety trochę schrzanione - dodają jedynie pewną możliwość obliczeń podczas danych operacji.

krótka dygresja o getterach i setterach i o tym dlaczego moim zdaniem nie wykorzystano w pełni ich potencjału

W JS każde pole składa się z nazwy i zestawu właściwości. Są to

enumerable
i

configurable
dostępne zawsze, niezależnie od typu pola;

value
i

writable
dostępne tylko jeśli pole jest tzw. Data Property; oraz

set
i

get
jeśli pole jest tzw. Accessor Property. Przy czym pole nie może być jednocześnie typu Data i Accessor, a więc albo mamy bezpośrednio trzymaną wartość, albo getter i setter. Ale jeśli chcemy coś zapisać przez

set
to i tak musimy użyć innego pola.

var foo = {

set bar(value) {

//jakas logika

}

};

Teraz wyobraźmy sobie, że piszemy

foo.bar = 5;
. Wywoływany jest nasz setter z parametrem

5
, dzieje się pełno jakiejś logiki np. transformującej nasz argument i powiedzmy że z naszej początkowej wartości 5 dostajemy "alamakota". Teraz chcemy to zapisać w obiekcie (w końcu wywołaliśmy setter) i... no właśnie, i co? Albo zapiszemy to w jakiejś innej publicznej własności (czyli zmiana pola

bar
da nam w rezultacie zmianę pola np.

baz
) albo skorzystamy z domknięcia (i wracamy do problemu prototypów i marnowania pamięci). Gdyby świat był lepszy to pole zawsze miałoby wartość

value
mogłoby mieć również opcjonalne funkcje

get
i

set
- wtedy wartość zwrócona przez setter zapisywałaby się do

value
, a do gettera dostarczana byłaby wartość z

value
, przy czym nie mielibyśmy żadnej możliwości bezpośredniego dobrania się do

value
.

Ech, mokry sen. No ale sami powiedzcie - nie miałoby to więcej sensu?

koniec dygresji

Także prywatności pól w tym podejściu nie przeskoczymy. Sorry, ziomki. settery i gettery mogą się przydać co najwyżej do okraszenia abstrakcją pewnych przejść obiektu między stanami.

Poprawmy jakoś te domknięcia

Jeśli chcemy żeby prywatne pola były zupełnie niewidoczne przed końcowym użytkownikiem (oraz przed debuggerem) to możemy chociaż spróbować z domknięć zrobić coś bardziej sensownego. Jedyna sensowna rzecz, jaka prawdopodobnie może być zrobiona w tej materii, to trzymać własności prywatne WSZYSTKICH instancji w domknięciu, np. w jakimś obiekcie - to daje nam możliwość skorzystania z nich w metodach upchniętych do prototypu, ale problemem pozostaje wtedy w jaki sposób określić które własności są którego obiektu. Przykładowa implementacja:

http://ideone.com/xyBtkN

Prywatne składowe są trzymane w domknięciu i dostęp do nich mają jedynie funkcje zadeklarowane w tym domknięciu. Jednakże to podejście ma pewną słabość - każda instancja ma publiczne pole

key
, które indeksuje prywatną hashmapę

privates
. Więc nasze prywatne zmienne nie są aż tak mocno chronione. Moim zdaniem to wystarczy tak czy siak, jeśli ktoś będzie zdeterminowany, żeby się dobrać do prywatnych składowych, to zrobi to. Możemy kombinować, żeby mu to utrudnić, ale czy jest sens?

Informuj, zamiast ograniczać

Kiedy uczyłem się JavaScriptu to strasznie jarałem się wszelakimi wzorcami wprowadzającymi hermetyzację do tego języka. Było to dla mnie niesamowite, jak ludzie potrafią błysnąć pomysłowością, żeby omijać stawiane im ograniczenia i uzyskiwać takie efekty, jakie chcą. Ale to wszystko ładnie wyglądało w teorii.

Potem przypomniałem sobie po co tak właściwie potrzebujemy hermetyzacji:

1. Możliwość lepszej kontroli stanu - tj. kiedy zmiana jednej rzeczy pociąga za sobą wykonanie dodatkowej logiki, która MUSI być wykonana

2. Możliwość oddzielenia API dostępnego dla innych od wewnętrznego API obiektu, żeby zmiana implementacji "pod spodem" nie pociągała za sobą konieczności wymiany kodu, który korzystał z danego obiektu

Pierwszy punkt możemy załatwić przez gettery i settery, bo do tego się nadają. W drugim przypadku, cóż, przydałyby się strasznie w tym języku interfejsy. Ale ich nie ma. Programiści to jednak, zazwyczaj, całkiem inteligentni ludzie. Jeśli im powiemy "ej, słuchaj, nie używaj tego pola, bo bezpośrednie grzebanie w nim może zaburzyć stan obiektu; tego też nie używaj, bo skąd wiesz, że w przyszłości go nie zastąpimy innym i wtedy twój kod przestanie działać" to powinni faktycznie tego nie robić, a jeśli robią no to są idiotami. Więc może by im tak po prostu powiedzieć? Popularną konwencją (i to na tyle popularną, że chociażby wspiera ją Visual Studio**) jest poprzedzanie nazw "prywatnych" własności obiektów znakiem podkreślenia: "". Pozwala to bez przeszkód debugować, pozwala na wsadzenie metod do prototypu, a dla programistów jest jasnym sygnałem, żeby tej własności bezpośrednio nie odczytywać. Możemy te prywatne właściwości dodatkowo uczynić nieenumerowalnymi.

Przyszłość

Na dzień dzisiejszy każdy obiekt, to hashmapa, w której kluczami są stringi. To zmieni się wraz z nadejściem ES6. W ES6 kluczami obiektów będą mogły być nie tylko stringi, będą nimi mogły być także specjalne obiekty (będzie to zupełnie nowy typ obiektów, tak jak teraz funkcje czy tablice), nazywane symbolami. A to otworzy drogę, do tworzenia unikalnych właściwości obiektów, które będzie można "wyciągnąć" tylko dysponując odpowiednim symbolem. A jeśli dodatkowo zamkniemy sobie taki symbol w domknięciu no to voila - będziemy w 100% mogli emulować prywatność! Zobaczmy przykład z Person, ulepszony o wykorzystanie symboli:

http://ideone.com/K6BN40

Metody z prototypu

Person
nie wyszukują już właściwości po nazwie - odwołują się do nich poprzez symbole. A ponieważ symbole siedzą sobie bezpiecznie w domknięciu i nie ma do nich dostępu z zewnątrz, to tym samym nikt nie jest w stanie bezpośrednio wyciągnąć pensji z instancji "klasy"

Person
. Hip hip, hurra! Nadal trochę naokoło, ale jest, mamy enkapsulację.

Jak zwykle zachęcam do zadawania pytań, jeśli się takowe pojawią. #javascript #programowanie

_______

* odsyłam do wikipedii - http://en.wikipedia.org/wiki/Funarg_problem

** w taki sposób, że pola poprzedzone "_" pokazuje w podpowiadaniu wyłącznie wewnątrz metod danej "klasy"
  • 25
@Marmite: ja akurat wolałbym żebyś miał pod jednym tagiem żeby było później łatwo znaleźć :) ale zapisze sobie na później bo jest pare ciekawych kwestii poruszonych o których wiem niezbyt dużo - a nie mam teraz czasu przeczytać
@fotexxx: (#) Rozważę kiedyś na pewno, to dopiero drugi tekścik który machnąłem a nie piszę za często, wiec też nie ma sensu pakowac się z tym w jakieś blogi ;) jak bedę pisac częściej, to na pewno o tym pomyślę
@Marmite: w sumie nie musiałby być "własny" tag - ja 3/4 powiadomień z #programowanie oznaczam jako przeczytane bo zazwyczaj to są proste pytania na które już ktoś odpowiedział - po 4-5 godzinach nie ma sensu już zaglądać do takich wpisów, wiele mi umyka przez to
@Marmite: Może chodzi o to żeby miec wszystko w jednym miejscu :-) i że będzie dalszy ciąg, ale jak rzadko wrzucasz to faktycznie nie ma to sensu

A wpis daje do ulubionych, bardzo fajnie napisane, szkoda że twórcy javascriptu nie wpadli na to żeby pójść w tę stronę w którą poszło adobe przy tworzeniu action scripta, który jest normalnie obiektowy (chociaż też nie jest idealny)

W sumie jestem w trakcie nauki
A wpis daje do ulubionych, bardzo fajnie napisane, szkoda że twórcy javascriptu nie wpadli na to żeby pójść w tę stronę w którą poszło adobe przy tworzeniu action scripta, który jest normalnie obiektowy (chociaż też nie jest idealny)


@b0lec: (#)Fun fact: Action Script jest oparty na ECMAScript 4 - tak miał kiedyś wyglądać JavaScript, ale się nie dogadali. Adobe poszło w tę specyfikację, Mozilla ją zarzuciła i rozpoczęto prace nad ECMAScript