Istnieją dwa modele implementacji klas i instancji w JavaScript: sposób prototypowania i sposób zamykania. Oba mają zalety i wady, i istnieje wiele rozszerzonych odmian. Wielu programistów i bibliotek ma różne podejścia i funkcje użytkowe do zarządzania klasami, aby opisywać niektóre bardziej brzydsze części języka.
W rezultacie w mieszanym towarzystwie będziesz mieć mieszankę metaklas, wszystkie zachowują się nieco inaczej. Co gorsza, większość materiałów instruktażowych dotyczących języka JavaScript jest okropna i służy pośredniemu kompromisowi obejmującemu wszystkie bazy, pozostawiając cię bardzo zdezorientowanym. (Prawdopodobnie autor jest również zdezorientowany. Model obiektowy JavaScript różni się bardzo od większości języków programowania, aw wielu miejscach jest źle zaprojektowany).
Zacznijmy od prototypowego sposobu . Jest to najbardziej natywny skrypt JavaScript, jaki można uzyskać: jest minimalny narzut kodu i instanceof będzie działał z instancjami tego rodzaju obiektu.
function Shape(x, y) {
this.x= x;
this.y= y;
}
Możemy dodać metody do utworzonej instancji new Shape
, pisząc je do prototype
wyszukiwania tej funkcji konstruktora:
Shape.prototype.toString= function() {
return 'Shape at '+this.x+', '+this.y;
};
Teraz, aby podklasować, tyle, ile można nazwać JavaScript. Robimy to, całkowicie zastępując tę dziwną magiczną prototype
właściwość:
function Circle(x, y, r) {
Shape.call(this, x, y); // invoke the base class's constructor function to take co-ords
this.r= r;
}
Circle.prototype= new Shape();
przed dodaniem do niego metod:
Circle.prototype.toString= function() {
return 'Circular '+Shape.prototype.toString.call(this)+' with radius '+this.r;
}
Ten przykład zadziała i zobaczysz podobny kod w wielu samouczkach. Ale stary, to new Shape()
jest brzydkie: tworzymy instancję klasy podstawowej, nawet jeśli nie ma zostać utworzony żaden prawdziwy Kształt. Zdarza się pracy w tym prostym przypadku, ponieważ JavaScript jest tak niechlujny: pozwala zerowe argumenty mają być przekazywane w, w tym przypadku x
i y
stać undefined
i są przypisane do prototypu this.x
ithis.y
. Gdyby funkcja konstruktora robiła coś bardziej skomplikowanego, padłaby płasko na twarz.
Musimy więc znaleźć sposób na stworzenie prototypowego obiektu zawierającego metody i inne elementy, które chcemy na poziomie klasy, bez wywoływania funkcji konstruktora klasy podstawowej. Aby to zrobić, musimy zacząć pisać kod pomocniczy. To najprostsze podejście, jakie znam:
function subclassOf(base) {
_subclassOf.prototype= base.prototype;
return new _subclassOf();
}
function _subclassOf() {};
To przenosi członków klasy podstawowej w prototypie do nowej funkcji konstruktora, która nic nie robi, a następnie używa tego konstruktora. Teraz możemy napisać po prostu:
function Circle(x, y, r) {
Shape.call(this, x, y);
this.r= r;
}
Circle.prototype= subclassOf(Shape);
zamiast tego new Shape()
zła. Mamy teraz akceptowalny zestaw prymitywów do zbudowanych klas.
Istnieje kilka udoskonaleń i rozszerzeń, które możemy rozważyć w ramach tego modelu. Na przykład tutaj jest wersja z cukrem syntaktycznym:
Function.prototype.subclass= function(base) {
var c= Function.prototype.subclass.nonconstructor;
c.prototype= base.prototype;
this.prototype= new c();
};
Function.prototype.subclass.nonconstructor= function() {};
...
function Circle(x, y, r) {
Shape.call(this, x, y);
this.r= r;
}
Circle.subclass(Shape);
Każda wersja ma tę wadę, że nie można odziedziczyć funkcji konstruktora, tak jak ma to miejsce w wielu językach. Więc nawet jeśli twoja podklasa nie wnosi nic do procesu budowy, musisz pamiętać o wywołaniu konstruktora podstawowego z dowolnymi argumentami, których żądała baza. Można to nieco zautomatyzować za pomocą apply
, ale nadal musisz napisać:
function Point() {
Shape.apply(this, arguments);
}
Point.subclass(Shape);
Dlatego powszechnym rozszerzeniem jest rozbicie elementów inicjalizacyjnych na ich własną funkcję, a nie na sam konstruktor. Ta funkcja może następnie dziedziczyć po podstawie:
function Shape() { this._init.apply(this, arguments); }
Shape.prototype._init= function(x, y) {
this.x= x;
this.y= y;
};
function Point() { this._init.apply(this, arguments); }
Point.subclass(Shape);
// no need to write new initialiser for Point!
Teraz mamy po prostu ten sam konstruktor funkcji konstruktora dla każdej klasy. Być może możemy przenieść tę funkcję do własnej funkcji pomocnika, abyśmy nie musieli jej pisać, na przykład zamiast Function.prototype.subclass
obracać ją i pozwalać funkcji klasy podstawowej wyrzucać podklasy:
Function.prototype.makeSubclass= function() {
function Class() {
if ('_init' in this)
this._init.apply(this, arguments);
}
Function.prototype.makeSubclass.nonconstructor.prototype= this.prototype;
Class.prototype= new Function.prototype.makeSubclass.nonconstructor();
return Class;
};
Function.prototype.makeSubclass.nonconstructor= function() {};
...
Shape= Object.makeSubclass();
Shape.prototype._init= function(x, y) {
this.x= x;
this.y= y;
};
Point= Shape.makeSubclass();
Circle= Shape.makeSubclass();
Circle.prototype._init= function(x, y, r) {
Shape.prototype._init.call(this, x, y);
this.r= r;
};
... który zaczyna wyglądać trochę bardziej jak inne języki, choć z nieco nieporadną składnią. Jeśli chcesz, możesz dodać kilka dodatkowych funkcji. Może chcesz makeSubclass
wziąć i zapamiętać nazwę klasy i podać jej domyślną nazwę toString
. Być może chcesz, aby konstruktor wykrył przypadkowe wywołanie bez new
operatora (co w innym przypadku często powodowałoby bardzo irytujące debugowanie):
Function.prototype.makeSubclass= function() {
function Class() {
if (!(this instanceof Class))
throw('Constructor called without "new"');
...
Być może chcesz przekazać wszystkich nowych członków i makeSubclass
dodać ich do prototypu, aby zaoszczędzić Ci Class.prototype...
dużo pisania . Robi to wiele systemów klasowych, np .:
Circle= Shape.makeSubclass({
_init: function(x, y, z) {
Shape.prototype._init.call(this, x, y);
this.r= r;
},
...
});
Istnieje wiele potencjalnych funkcji, które możesz uznać za pożądane w systemie obiektowym i nikt tak naprawdę nie zgadza się na jedną konkretną formułę.
Zatem sposób zamknięcia . Pozwala to uniknąć problemów związanych z dziedziczeniem opartym na prototypach JavaScript, ponieważ w ogóle nie używa się dziedziczenia. Zamiast:
function Shape(x, y) {
var that= this;
this.x= x;
this.y= y;
this.toString= function() {
return 'Shape at '+that.x+', '+that.y;
};
}
function Circle(x, y, r) {
var that= this;
Shape.call(this, x, y);
this.r= r;
var _baseToString= this.toString;
this.toString= function() {
return 'Circular '+_baseToString(that)+' with radius '+that.r;
};
};
var mycircle= new Circle();
Teraz każda pojedyncza instancja Shape
będzie miała własną kopię plikutoString
metody (i dowolne inne metody lub inne elementy klasy, które dodamy).
Złą rzeczą w tym, że każda instancja ma własną kopię każdego członka klasy jest to, że jest mniej wydajna. Jeśli masz do czynienia z dużą liczbą podklasowanych instancji, dziedziczenie prototypowe może ci lepiej służyć. Również wywoływanie metody klasy podstawowej jest nieco denerwujące, jak widać: musimy pamiętać, jaka była metoda, zanim konstruktor podklasy ją nadpisał, w przeciwnym razie się zgubi.
[Również dlatego, że nie ma tutaj dziedziczenia, instanceof
operator nie będzie działał; gdybyś tego potrzebował, musisz stworzyć własny mechanizm wąchania klas. Podczas gdy ty mógł bawić obiekty prototyp w podobny sposób jak z prototypowego dziedziczenia, jest to trochę skomplikowane i naprawdę nie warto go tak aby uzyskaćinstanceof
pracę.]
Zaletą każdej instancji mającej własną metodę jest to, że metoda może być następnie powiązana z konkretną instancją, która jest jej właścicielem. Jest to przydatne ze względu na dziwny sposób wiązania JavaScript this
w wywołaniach metod, który ma efekt, że jeśli odłączysz metodę od jej właściciela:
var ts= mycircle.toString;
alert(ts());
wtedy this
w metodzie nie będzie instancji Circle zgodnie z oczekiwaniami (w rzeczywistości będzie to window
obiekt globalny , powodujący powszechne problemy z debugowaniem). W rzeczywistości zwykle dzieje się tak, gdy metoda jest pobierana i przypisywana do setTimeout
, onclick
lubEventListener
w ogóle.
Prototypowym sposobem jest dołączenie zamknięcia dla każdego takiego zadania:
setTimeout(function() {
mycircle.move(1, 1);
}, 1000);
lub w przyszłości (lub teraz, jeśli włamiesz się do Function.prototype), możesz to również zrobić za pomocą function.bind()
:
setTimeout(mycircle.move.bind(mycircle, 1, 1), 1000);
jeśli twoje instancje są wykonywane w sposób zamykający, wiązanie odbywa się za darmo przez zamknięcie nad zmienną instancji (zwykle wywoływaną that
lub self
, choć osobiście odradzam tę ostatnią, ponieważ self
ma ona już inne, inne znaczenie w JavaScript). Nie otrzymujesz argumentów 1, 1
z powyższego fragmentu za darmo, więc nadal będziesz potrzebować kolejnego zamknięcia lubbind()
jeśli musisz to zrobić.
Istnieje również wiele wariantów metody zamknięcia. Możesz this
całkowicie pominąć , tworząc nowy that
i zwracając go zamiast new
operatora:
function Shape(x, y) {
var that= {};
that.x= x;
that.y= y;
that.toString= function() {
return 'Shape at '+that.x+', '+that.y;
};
return that;
}
function Circle(x, y, r) {
var that= Shape(x, y);
that.r= r;
var _baseToString= that.toString;
that.toString= function() {
return 'Circular '+_baseToString(that)+' with radius '+r;
};
return that;
};
var mycircle= Circle(); // you can include `new` if you want but it won't do anything
Który sposób jest „właściwy”? Obie. Który jest „najlepszy”? To zależy od twojej sytuacji. FWIW Mam tendencję do tworzenia prototypów w celu rzeczywistego dziedziczenia JavaScript, gdy robię rzeczy zdecydowanie OO, i zamykania dla prostych efektów stronicowania.
Oba sposoby są jednak przeciwne do intuicji większości programistów. Oba mają wiele potencjalnych niechlujnych odmian. Poznasz oba (jak również wiele schematów pośrednich i ogólnie zepsutych), jeśli będziesz używać kodu / bibliotek innych osób. Nie ma jednej ogólnie akceptowanej odpowiedzi. Witamy w cudownym świecie obiektów JavaScript.
[Jest to część 94 Dlaczego JavaScript nie jest moim ulubionym językiem programowania.]