Mogę się wypowiedzieć nt. JavaScriptu z racji mojej znajomości tego języka.
W JavaScripcie, new F()
to coś innego niż F()
. Oba są wywołaniami funkcji, ale operator new
, oprócz wywoływania funkcji, robi coś ekstra: ustawia kontekst (this
) wewnątrz F, pilnuje, by F zwróciło obiekt i umożliwia utworzenie obiektu dziedziczącego po prototypie.
JavaScript to język dynamiczny. Nie da się statycznie wywnioskować, że pisząc F()
chcemy wywołać konstruktor -- może po prostu chcemy wywołać funkcję F? Wiele funkcji pisze się tak, że można je wywołać zarówno z new
, jak i bez new
i efekt będzie ten sam. Od tej reguły są jednak wyjątki. Nawet wśród funkcji wbudowanych. Np. funkcja Date()
. Wywołana z new
zwraca obiekt. Bez new
-- ciąg znaków.
To powiedziawszy, operator new
można uznać za jeden z błędów projektowych JavaScriptu. new
zostało dodane do języka po to, by upodobnić się do języków z dziedziczeniem klasycznym, w szczególności do Javy. To błąd, bo JavaScript nie posiada dziedziczenia klasycznego. Nie udostępnia programiście takiego pojęcia jak "klasa". Specyfikacja języka definiuje co prawda ukrytą, roboczą własność [[Class]]
, ale nazywa jej wartość "klasyfikacją obiektu", a nie klasą.
JavaScript, zamiast dziedziczenia klasycznego, ma dziedziczenie prototypowe. Jest ono prostsze i -- można się kłócić -- bardziej eleganckie, a na pewno minimalistyczne. Nie ma tu podziału na "obiekty" (instancje) i jakieś ezoteryczne "klasy". Nie. W JavaScripcie są tylko obiekty. Obiekty dziedziczą po obiektach-prototypach. I tyle.
Gdzie tu miejsce dla new
?
No... właśnie specjalnie go nie ma.
Teoretycznie (jak zaleca np. Douglas Crockford), gdy tworzymy obiekt, powinniśmy powiedzieć: daj mi pusty obiekt myObj
dziedziczący po obiekcie myProto
(jak "prototype"). W ECMAScript 5 wprowadzono funkcję Object.create
, która pozwala nam zrobić właśnie to (pomijam w tym poście pozostałe funkcje Object.create
):
var myObj = Object.create(myProto);
I dzięki tej małej funkcji, operator new
robi nam się absolutnie niepotrzebny. Wywołanie Object.create
możemy ukryć w naszych "konstruktorach", czy też "metodach wytwórczych" (że pozwolę je sobie tak nazwać):
var programmerPrototype = {
// definicje funkcji i pól składowych wspólnych dla wszystkich programistów
writeCode: function() {
// ...
}
};
function createProgrammer(nameArg, languageArg) {
var that = Object.create(programmerPrototype);
// różnice pomiędzy programistami
that.name = nameArg;
that.language = languageArg;
return that;
}
// tworzenie instancji
var uncleBob = createProgrammer("Robert", "Java");
uncleBob.writeCode();
Powyższy wzorzec jest prosty, ale bardzo klasyczny -- prototyp jest stały i wygląda z grubsza jak definicja klasy. Można go równie dobrze napisać, nie używając Object.create
i używając operatora new
. Poniżej ekwiwalent przykładu z Object.create
:
Programmer.prototype = {
// definicje funkcji i pól składowych wspólnych dla wszystkich programistów
writeCode: function() {
// ...
}
};
function Programmer(nameArg, languageArg) {
// różnice pomiędzy programistami
this.name = nameArg;
this.language = languageArg;
}
// tworzenie instancji
var uncleBob = new Programmer("Robert", "Java");
uncleBob.writeCode();
Czyli: tu new
nie jest takie złe, bo każdy programista ma prototyp, który jest dokładnie tym samym obiektem.
W bardziej skomplikowanym kodzie dość często zdarza mi się jednak stosować dynamicznie tworzone prototypy lub nawet ich łańcuchy. Chcę móc dynamicznie dobierać rodziny obiektów, każda z własnym prototypem.
Tu już notacja z new
oraz Konstruktor.prototype
jest cholernie niewygodna i pokraczna. Muszę użyć czegoś w rodzaju Object.create
. Ponieważ nie wszystkie przeglądarki mają Object.create
, muszę sobie taką funkcję napisać...
function createWithPrototype(proto) {
function Dummy() {
}
Dummy.prototype = proto;
return new Dummy();
}
// wywołanie:
var myObj = createWithPrototype(myProto);
Tutaj widać, że Object.create
i new
są w JavaScripcie ekwiwalentne (a tak naprawdę Object.create
jest potężniejsze, bo jako drugi parametr może przyjąć tzw. deskryptor obiektu -- ale mniejsza o to). Czyli: new
jest w nowoczesnym JavaScripcie zbędne. Crockford zdaje się sugerować, by i w starym JavaScripcie użyć new
tylko raz: pisząc createWithPrototype()
;).
Normalnie, gdy ktoś napisał funkcję z myślą o tym, by była używana z new
, a my o new
zapomnimy (silnik JS o tym nie ostrzeże, bo new
nie zawsze jest obowiązkowe!), to stanie się tragedia.
Załóżmy, że pominęliśmy new
wywołując zdefiniowany wyżej konstruktor Programmer
:
var bob = Programmer("Kmieciu", "Delphi");
Wewnątrz funkcji Programmer
doczepiamy jakieś własności do obiektuthis
. Jeśli funkcja jest wywołana z new
, this
wskazuje w niej na nowo utworzony obiekt (czyli programistę). Jeśli new
pominiemy, to mamy zwykłe wywołanie funkcji globalnej. I this
wskazuje na obiekt globalny, którym w przeglądarce jest window
. Więc nasz konstruktor (wywołany jak zwykła funkcja globalna), zamiast pól obiektu bob
, tworzy pola obiektu window
: window.name
i window.language
, czyli zmienne globalne name
i language
. Tragedia.
Gdybyśmy korzystali z Object.create
, bylibyśmy bezpieczni.