Aktywne Wpisy
Lelkomtu +69
niochland +142
W Warszawie już od tygodnia działa Strefa Czystego Transportu.
Jakoś cicho o tym na Wykopie, a przecież świat miał się zawalić, stąd też pytanie dla mieszkańców #warszawa
1. Ile biednych ludzi nie dojechało swoimi dupowozami na czas do pracy?
2. Ile smutnych ludzi ma problem, żeby dowieźć dziecko do przedzszkola?
3. Ile dup prawaków pękło?
Jakoś cicho o tym na Wykopie, a przecież świat miał się zawalić, stąd też pytanie dla mieszkańców #warszawa
1. Ile biednych ludzi nie dojechało swoimi dupowozami na czas do pracy?
2. Ile smutnych ludzi ma problem, żeby dowieźć dziecko do przedzszkola?
3. Ile dup prawaków pękło?
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
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)
$(element).addClass(name)
?!
Car := Object clone
Car drive := method(by, by
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 :)
@Nikczemny: A(#) Ale co WAT? :>
instanceof
```**
1. Operator ten sam w sobie nie ma algorytmu działania. Tak naprawdę wywołuje on wewnętrzną metodę