Wpis z mikrobloga

W JavaScript nie ma klas, są tylko mechanizmy je imitujące. Kieruję ten wywód do każdego, kto używa takiej terminologii (klasa, instancja klasy) w odniesieniu do funkcji i obiektów w JS i nie jest świadom o czym tak naprawdę mówi. No chyba że jest świadom a mówi tak tylko dla ukazania pewnego skrótu myślowego - ale i tak polecam przeczytać, zawsze można wyciągnąć z tego jakąś wiedzę.

Do napisania tego natchnęła mnie ostatnio zasłyszana w pracy rozmowa, podczas której jeden z programistów zapytał naszego przełożonego mniej-więcej "czy ten obiekt ma być instancją jakiejś klasy czy może ma to być zwykły obiekt?" na co przełożony zgodnie z prawdą odpowiedział "zależy co w JavaScript rozumiesz poprzez 'instancję klasy' a co poprzez 'zwykły obiekt'".

---KLASY---

Najsampierw zacznijmy od tego, że jeśli ktoś nie wie, to klasa jest szablonem dla obiektów (jej instancji). Obiekty te zawierają dokładnie te same zbiory zachowań i atrybutów, a różnią się tylko wartościami w nich przetrzymywanymi. A przede wszystkim intuicyjnie można wyczuć między nimi pewien związek. Czujemy, że 2 instancje klasy Samochód w pewien sposób łączy pewna przynależność, pewna abstrakcyjna odróżnialność od instancji innych klas, np. od instancji klasy KostkaMasła.

W JavaScript nie ma takich rzeczy. Mamy obiekty, które właściwie są "tylko" kontenerem na właściwości. Z technicznego punktu widzenia obiekt w JS to nic innego jak hashmapa, która podlega dziedziczeniu (po innych obiektach).

O jakimkolwiek "klasowym" podejściu w JSie jakim znamy dzisiaj* nie byłoby w ogóle mowy gdyby nie pewna cecha tego języka, jedna z najlepszych moim zdaniem rzeczy w nim zawartych. Jest to mianowicie możliwość uruchomienia prawie dowolnej funkcji w kontekście prawie dowolnego* obiektu. "W kontekście obiektu X" oznacza, że w trakcie wykonywania tej funkcji słowo kluczowe "this" będzie referencją na obiekt X. Ważny z punktu widzenia klas jest także operator

new
, służący do tworzenia "instancji" "klas".

---INSTANCJONOWANIE "KLAS" W JS---

Wyjaśnijmy sobie może właściwie czym tak naprawdę jest operator

new
. W rzeczywistości jego działanie odbiega trochę od abstrakcyjnego myślenia o tworzeniu nowej instancji klasy. Operator ten wywołuje tak naprawdę wewnętrzną metodę

[[Construct]]
danej funkcji, która wykonuje następujące rzeczy: (zakładamy, ze wywołujemy

new Foo(arg1, arg2...);
)

1. Stwórz nowy obiekt

2. Ustaw łańcuch prototypów nowo tworzonego obiektu (sprawdź co kryje się we własności

Foo.prototype
. Jeśli jest tam obiekt, ustaw go jako prototyp nowo tworzonego obiektu. W przeciwnym wypadku ustaw prototyp nowo tworzonego obiektu na obiekt znajdujący się w

Object.prototype
)

3. Wywołaj Foo w kontekście nowo tworzonego obiektu, przekazując jej argumenty przekazane w konstruktorze. Oznacza to, ze zostanie wykonana logika funkcji Foo, jedynie z tą różnicą że

this
wewnątrz niej będzie wskazywać na nowo tworzony obiekt.

4. Upewnij się, że operator new zwróci obiekt (sprawdź czy wywołanie funkcji Foo, które nastąpiło w pkt 3. zwróciło obiekt. Jeśli tak, zwróć go. W przeciwnym wypadku zwróć nowo tworzony obiekt)

Całość da się zapisać w natywnym ES-5 i wygląda to tak:

var New = function(constructor) {

 var obj, result,

     args = Array.prototype.slice.call(arguments, 1); //args bedzie zawierac parametry, ktore chcemy przekazac do konstruktora

 if(constructor.prototype && typeof constructor.prototype === "object") {

  obj = Object.create(constructor.prototype);

 } else {

  obj = Object.create(Object.prototype); //rownowazne zapisowi obj = {}

 }

 result = constructor.apply(this, args);

 if(result && typeof result === "object") {

  return result;

 } else {

  return obj;

 }

};

//przykladowe wywolanie analogiczne do var foo = new Foo("bar")

var foo = New(Foo, "bar");

Co jest ważne: przede wszystkim zauważmy, że funkcja Foo może

a) całkowicie olać to, że jest konstruktorem i wykonać dowolną inną logikę, zupełnie niemającą wpływu na nowo tworzony obiekt.

function Foo() {

 console.log("JESTE KONSTRUKTORE");

}

b) zwrócić zupełnie inny obiekt niż nowo tworzony (i to też będzie legitne - patrz pkt 4. algorytmu dla

new
)

Pierwszy z powodów dla których nie powinno się używać terminologii klasowej w JS: funkcje to zwykłe funkcje.

---PRZYNALEŻNOŚĆ: PODOBIEŃSTWO OBIEKTÓW---

Przejdźmy do kolejnego podpunktu, odnośnie pewnej "przynależności" obiektów, jednoznacznie identyfikującej do jakiej klasy należy. Weźmy na ten przykład takie coś:

var Foo = function(something) {

 this.something = something;

};

stwórzmy dwie "instancje" "klasy" Foo:

var obj1 = new Foo("bar"), obj2 = new Foo("baz");
A teraz zróbmy takie coś:

delete obj1.something;

obj1.somethingElse = 1;

obj2.anything = false;

Czy o tych dwóch obiektach nadal możecie powiedzieć, że są instancjami klasy Foo? Są to na tę chwilę zupełnie różne obiekty, które mogą podlegać dalszym modyfikacjom. Mimo że były to "instancje" tej samej "klasy", teraz ciężko już chyba coś takiego o nich powiedzieć, prawda?

---PRZYNALEŻNOŚĆ: DZIEDZICZENIE---

Ale nadal mają jedną część wspólną. Mimo że wyglądają zupełnie różne, ktoś może powiedzieć, że należą do "klasy" Foo, ponieważ przechodzą test operatora

instanceof
. Rzeczywiście, poniższe wywołania nie zwrócą błędu:

console.assert(obj1 instanceof Foo);

console.assert(obj2 instanceof Foo);

Musimy jednak zajrzeć JavaScriptowi pod maskę po raz kolejny i dowiedzieć się co tak naprawdę sprawdza operator

instanceof
. Algorytm jest taki: (niech

O
będzie lewym operandem, a

C
prawym)

1. Niech

P
będzie prototypem

O
2. Jeśli

P === null
, zwróć

false
i zakończ działanie

3. Jeśli

P === C.prototype
zwróć

true
i zakończ działanie

4. Wróć do punktu 1

Innymi słowy:

instanceof
bierze sobie łańcuch prototypów obiektu

O
, skanuje każdy jego element i porównuje te elementy z obiektem ukrywającym się pod

C.prototype
. Najkrócej jak się da - sprawdza czy

O
dziedziczy po

C.prototype
warto pamiętać, że własność prototype danego obiektu oraz jego prototyp to są 2 różne rzeczy. Nazewnictwo moim zdanem wybrano niezbyt trafnie, bo nowych potrafi to skonfundować


A więc obiekty

obj1
i

obj2
, aczkolwiek zupełnie różne, przechodzą test operatora

instanceof
ponieważ oba dziedziczą po

Foo.prototype
. Czy jednak to determinuje ich przynależność do "klasy" Foo? A gdybym zmienił to, co kryje się w

Foo.prototype
?

var obj3 = new Foo();

Foo.prototype = {

 constructor: Foo

};

var obj4 = new Foo();

Zmieniłem obiekt kryjący się w

Foo.prototype
na identyczny jak był**, ale jednak inny obiekt. A to sprawia, że

obj4
przejdzie test

instanceof
, ale

obj3
już nie. Jeśli dorzucić do tego fakt, ze mogę sobie je dowolnie pozmieniać to mimo stworzenia ich przez "klasę"

Foo
nie mają ze sobą nic wspólnego.

To jest właśnie kluczowe dla pojęcia "klas" w JS: tak naprawdę to tylko wykonywania pewnych czynności w kontekście zupełnie różnych obiektów. Czynności które nawet nie muszą prowadzić do powstania nowych, powiązanych ze sobą obiektów.

--NIE TAK SZYBKO---

Ale czy da się coś z tymi fantami zrobić? Okazuje się, że da. Wymienione przeze mnie mankamenty częściowo da się załagodzić, przyblizając tym samym JS do posiadania klas. Co można zrobić?

Punkt 1: upewnijmy się, że funkcja zostanie zawsze wywołana w roli konstruktora. Służy do tego prosty warunek:

function Foo() {

 if(!(this instanceof Foo)) {

  return new Foo();

 }

(...)

};

Wykorzystujemy tutaj fakt, że wywołanie funkcji

Foo
w algorytmie operatora

new
występuje dopiero w 3. punkcie - już po stworzeniu nowego obiektu. Możemy zatem zbadać czy ten nowy obiekt dziedziczy po

Foo.prototype
*** i, jeśli nie, wymusić wywołanie funkcji

Foo
w roli konstruktora oraz zwrócić rezultat.

Punkt 2: zabrońmy zmiany wyglądu obiektów. Używając API ES5 możemy w prosty sposób zablokować obiekty przed dodawaniem do nich nowych właściwości oraz usuwaniem już istniejących. Służy do tego metoda

Object.seal()
. Przekazany do niej obiekt zostanie oznaczony jako

non-extensible
(nie będzie można dodać nowych własności), a wszystkim jego własnościom parametr

configurable
zostanie ustawiony na

false
(żadnej istniejącej nie będzie już można usunąć). Przed końcem naszej funkcji konstruującej dorzucamy zatem

Object.seal(this)
, tym samym zablokuje to możliwość dowolnej zmiany "instancji" wymuszając ich podobieństwo.

Punkt 3: zabrońmy zmieniać własności

prototype
konstruktora. Spowoduje to, że jego "instancje" będą zawsze posiadać ten sam prototyp. Możemy to zrobić, używając metody

Object.defineProperty()
. Użyjmy zatem

Object.defineProperty(Foo, "prototype", {

 writable: false,

 configurable: false,

 value: {

  bar: "baz"

 }

});

Do

Foo.prototype
zostanie wpisane to, co ukrywa się pod

value
. Opcja

configurable
ustawiona na

false
sprawi, że nie będzie można usunąć własności, a

writable
że nie będzie można nic do niej przypisać.

Wyżej wymienione sposoby mocno ograniczą rzeczy, o których wcześniej pisałem, przybliżając trochę "klasy" JS do rzeczywistych klas, znanych z innych języków. Jedynym już chyba problemem jest hermetyzacja (którą da się rozwiązać albo stosując domknięcia, albo pisząc w TypeScript ) oraz tego, ze nie ma i nie da się (przynajmniej na razie) w JS zrobić destruktorów.

W razie pytań zapraszam do ich zadawania. #javascript #programowanie #tldr #dlugipost

________

* w ES6 zostaną wprowadzone "klasy", jednak warto mieć świadomość że będzie to tylko lukier syntaktyczny - wszystko odbywać się będzie nadal w oparciu o funkcje konstruujące i prototypy

wyjątki stanowią funkcje, które zostały utworzone poprzez użycie metody

Function.prototype.bind
- one mają z góry ustaloną, niezmienialną wartość kryjącą się pod

this
* niektóre z obiektów wbudowanych np. w przeglądarki mogą sprawiać problem - nie będzie się dało wywołać funkcji w ich kontekście lub niektórych funkcji nie będzie można wywołać w innym kontekście niż ich

** domyślnie każda zadeklarowana funkcja otrzymuje własność

prototype
, w której znajduje się obiekt posiadający własność

contructor
, będącą referencją na tę funkcję

*** zbadanie operatorem

instanceof
przepuści dowolny obiekt, mający w swym łańcuchu prototypów obiekt

Foo.prototype
. Można ograniczyć to tylko do bezpośredniego dziedziczenia, zmieniając warunek na

if(Object.getPrototypeOf(this) === Foo.prototype)
  • 19
@lucku: (#) Bez sensu trochę, nie będę zakładać bloga dla umieszczenia na nim jednego wpisu :D jeśli najdzie mnie ochota na pisanie więcej takich postów (znaczy, ochota zawsze jest, tylko tematów mało) to rozważą. Tymczasem mogę ew. wrzucić na zoba.to czy inną Pokazywarkę, jeśli to coś zmieni ;)
@Marmite: Częściowo masz rację, ale w innych językach też bywa, że nie ma czegoś takiego jak klasa, a mimo to się o nich mówi. Np. w Rubym każda klasa jest singletonem przypisanym do stałej. Jednak lepszym przykładem będzie Io (który ma w taki sam sposób rozwiązaną obiektowość jak JS). Każdy obiekt jest jednocześnie klasą po której można "dziedziczyć", np.:

Car := Object clone

Car drive := method(by, by println)

maluch :=
@Hauleth: (#) No mówi się i przecież nikomu nie zabronię, mogą to nawet nazywać bulbulatorami jak lubią :D chciałem tylko uczulić na świadomość jak to wszystko działa, jak jest zaimplementowane i dlaczego jednak bez tego co wymieniłem w ostatnim akapicie JSowym "klasom" trochę brakuje do takich "typowych" (C++, Java, C#) klas.
@Marmite: dzięki za wpis, jako osobie która przyszła do JSa z Pythona przez jQuery brakowało mi takiego pełnego i łopatologicznego wyjaśnienia :)

A co do bloga, to obczaj sobie w G pod hasłem "static blog engine", przykładowo Jekyll, którego bezproblemowo możesz hostować za free na Githubie :)
Mała errata odnośnie operatora

instanceof
```**

1. Operator ten sam w sobie nie ma algorytmu działania. Tak naprawdę wywołuje on wewnętrzną metodę

[[HasInstance]]

funkcji (prawego operandu) przekazując jej lewy operand. Jest to ważne, bo w sumie funkcje stworzone przez
Function.prototype.bind

mają inną metodę
[[HasInstance]]

(kierującą do metody
[[HasInstance]]

oryginalnej funkcji)

2. W podanym przeze mnie algorytmie jest mały błąd. Punkt 4. powinien brzmieć "podstaw

P

pod
O
```
i wróć do punktu