Rozmawiając ze znajomymi, którzy również są webdeveloperami, zauważyłem, że JavaScript jest jednym z najbardziej znienawidzonych języków. Jest kilka tego powodów, najważniejsze z nich to brak jednolitej obsługi w w przeglądarkach internetowych oraz brak klas. Pierwszy powód jest bezdyskusyjny, chociaż można zauważyć, że z upływem czasu jest coraz lepiej i minęły już te dni gdy pisało się kod osobno dla każdego browsera. Obsługa JavaScriptu jest coraz lepsza wśród przeglądarek i nawet Microsoft wziął się na poważnie do roboty z IE9. Natomiast argument o braku klas w Javascripcie uważam za trochę chybiony i wynikający z braku wiedzy o modelu obiektowym jaki został zaimplementowany w tym języku.
Javascript jest językiem obiektowo-funkcyjnym. Wszystko w Javascripcie jest obiektem łącznie z funkcjami. Fakt, że w Javascripcie nawet funkcje są obiektami czyni z niego język funkcyjny. Z elementów języka funkcyjnego posiada on również domknięcia i funkcje anonimowe co wynika bezpośrednio z tego, że funkcje są obiektami. Kilka linijek kodu by to dowieść:
//funkcja nazwana
function foo(){
return 1;
}
console.log(foo);
//wyswietli 'foo()'
//funkcja anonimowa przypisana do stalej - stala zmienna ktorej wartosci nie mozna zmienic(immutable)
const bar = function(){
return 1;
}
console.log(bar);
//zwraca 'function()'
const b = 1;
//zwroci exception redeclaration of const b
//funkcja ktora zwraca obiekt funkcji
function baz(){
return function(){
return 1;
};
}
Wracając do naszych klas, należałoby się najpierw zastanowić czym jest klasa ? W językach o „tradycyjnym” modelu obiektowym (php, java, c++), klasa jest szablonem z którego tworzone są egzemplarze obiektów (instancje). Obiekty te mają ten sam sposób zachowania (współdzielą metody), różnią się jedynie wewnętrznym stanem (właściwości). Tak naprawdę są to języki zorientowane na klasy a nie na obiekty, żeby stworzyć jakikolwiek obiekt, trzeba najpierw zdeklarować klasę. W Javascripcie, dwa obiekty są tej samej klasy jeżeli mają ten sam konstruktor (prototyp).
//konstruktor klasy foo
function foo(){
this.a = 'a'
this.b = 'b'
}
var a = new foo();
var b = new foo();
console.log(a instanceof foo);
//zwroci true
console.log(b instanceof foo);
//zwroci true
Fakt ten wiążę się również z mechanizmem dziedziczenia. Jeżeli chcemy by klasa bar dziedziczyła z klasy foo to znaczy to nie mniej nie więcej tyle, że konstruktor klasy bar powinien być wywołany po konstruktorze klasy foo. Wszystko wyjaśni poniższy przykład:
//konstruktor klasy foo
function foo(){
this.a = 'a'
this.b = 'b'
}
//konstruktor klasy bar
function bar(){
this.c = 'c'
this.d = 'd'
}
//konstruktor klasy baz
function baz(){
this.e = 'e'
this.f = 'f'
}
//bar dziedziczy z foo
bar.prototype = new foo();
//baz dziedziczy z bar
baz.prototype = new bar();
x = new baz();
console.log(x);
console.log(x instanceof foo);
Jest to mechanizm pojedynczego dziedziczenia, jak wiadomo wielodziedziczenie jest problematyczne i w większości języków zrezygnowano z niego. Natomiast Javascript dzięki swojej funkcyjności pozwala na elastyczne komponowanie obiektów z różnych funkcji. Można np. wsiąść metodę z jednego obiektu, bądź globalnego konstekstu i przypisać ją do jakiejś klasy bądź obiektu. W Javascripcie zmienna „this” zawsze odnosi się do aktualnego kontekstu. Przykład:
//funkcja foofoo
var foofoo = function(){
return this.a;
};
//konstruktor klasy foo
function foo(){
this.a = 'a'
this.b = 'b'
this.foofoo = foofoo;
}
//konstruktor klasy bar
function bar(){
this.c = 'c'
this.d = 'd'
this.a = 'g';
this.foofoo = foofoo;
}
x = new bar()
console.log(x.foofoo());
//zwroci 'g'
z = new foo()
console.log(z.foofoo());
//zwroci 'a'
Jedną z rzeczy, którą zarzuca się JavaScriptowi jest brak enkapsulacji. Jest to oczywiście nieprawda – JS posiada enkapsulacje ale realizowaną w inny sposób. Jak już wcześniej wspomniałem między wierszami, istnieje coś takiego jak kontekst wywołania funkcji. Każda funkcja anonimowa jest też domknięciem (closure) i pamięta kontekst swojego utworzenia, więc jeżeli w konstruktorze zadeklarujemy zmienną słowem kluczowym var, to metoda obiektu ją zapamięta. W ten sposób realizuje się enkapsulacje. Przykład:
//konstruktor klasy foo
function foo(){
this.a = 'a'
this.b = 'b'
var privVar = 'a';
this.bar = function(){
return privVar;
}
}
z = new foo()
console.log(z.privVar);
//zwroci undefined
console.log(z.bar());
//zwroci 'a'
Ostatnią sprawą jaką chciałbym poruszyć są Singletony. W językach zorientowanych na klasy trzeba uzywać metod statycznych i innych trików. Natomiast w JS wystarczy zadeklarować anonimowy obiekt przypisany do globalnej zmiennej:
Singleton = {
a: 'a',
b: 'b',
c: function(){
return this.a;
}
}
console.log(Singleton.c());
Mam nadzieje, że ten artykuł przyczyni się chociażby w jakimś stopniu do zrozumienia JS, który jest przez wielu wyklęty tak naprawdę tylko ze względu na swoją odmienność.
Wszystkie przykłady testowałem na Firefoxie 3.6.6 + Firebug. Jak zwykle czekam na feedback i wszelkie pytania .