Wiem, że ta odpowiedź jest spóźniona o 3 lata, ale naprawdę uważam, że obecne odpowiedzi nie dostarczają wystarczających informacji na temat tego, w jaki sposób dziedziczenie prototypowe jest lepsze niż dziedziczenie klasyczne .
Najpierw zobaczmy najczęstsze argumenty, które programiści JavaScript twierdzą w obronie dziedziczenia prototypowego (biorę te argumenty z bieżącej puli odpowiedzi):
- To proste.
- Jest potężny.
- Prowadzi to do mniejszego, mniej zbędnego kodu.
- Jest dynamiczny i dlatego jest lepszy dla dynamicznych języków.
Teraz wszystkie te argumenty są prawidłowe, ale nikt nie zadał sobie trudu, aby wyjaśnić, dlaczego. To tak, jakby powiedzieć dziecku, że nauka matematyki jest ważna. Jasne, że tak, ale dziecku na pewno to nie obchodzi; i nie możesz zrobić dziecka takiego jak matematyka, mówiąc, że to ważne.
Myślę, że problem z dziedziczeniem prototypów polega na tym, że wyjaśniono go z perspektywy JavaScript. Uwielbiam JavaScript, ale prototypowe dziedziczenie w JavaScript jest złe. W przeciwieństwie do klasycznego dziedziczenia istnieją dwa wzorce dziedziczenia prototypowego:
- Prototypowy wzór dziedziczenia prototypowego.
- Wzorzec konstruktora dziedziczenia prototypowego.
Niestety JavaScript wykorzystuje wzorzec konstruktora dziedziczenia prototypowego. Dzieje się tak dlatego, że kiedy JavaScript został utworzony, Brendan Eich (twórca JS) chciał, aby wyglądał jak Java (która ma klasyczne dziedzictwo):
Pchaliśmy go jako młodszego brata do Javy, ponieważ uzupełniającym językiem, takim jak Visual Basic, był C ++ w rodzinach językowych Microsoft w tym czasie.
Jest to złe, ponieważ kiedy ludzie używają konstruktorów w JavaScript, myślą o konstruktorach dziedziczących po innych konstruktorach. To jest źle. W prototypowym dziedzictwie obiekty dziedziczą po innych obiektach. Konstruktorzy nigdy nie pojawiają się na zdjęciu. To myli większość ludzi.
Ludzie z języków takich jak Java, która ma klasyczne dziedzictwo, stają się jeszcze bardziej zdezorientowani, ponieważ chociaż konstruktory wyglądają jak klasy, nie zachowują się jak klasy. Jak stwierdził Douglas Crockford :
Ta pośrednia intencja miała uczynić język bardziej znanym klasycznie wyszkolonym programistom, ale nie udało się tego zrobić, jak widać z bardzo niskiej opinii programistów Java o JavaScript. Wzorzec konstruktora JavaScript nie spodobał się klasycznej publiczności. Zasłonił również prawdziwy prototypowy charakter JavaScript. W rezultacie bardzo niewielu programistów wie, jak skutecznie używać języka.
Masz to. Prosto z pyska konia.
Prawdziwe dziedzictwo prototypowe
Dziedzictwo prototypowe dotyczy przede wszystkim przedmiotów. Obiekty dziedziczą właściwości z innych obiektów. To wszystko. Istnieją dwa sposoby tworzenia obiektów przy użyciu dziedziczenia prototypowego:
- Utwórz zupełnie nowy obiekt.
- Sklonuj istniejący obiekt i rozszerz go.
Uwaga: JavaScript oferuje dwa sposoby klonowania obiektu - delegowanie i konkatenacja . Odtąd będę używać słowa „klon”, aby odnosić się wyłącznie do dziedziczenia poprzez przekazanie, a słowa „kopiować”, aby odnosić się wyłącznie do dziedziczenia poprzez konkatenację.
Dość gadania. Zobaczmy kilka przykładów. Powiedz, że mam okrąg o promieniu 5
:
var circle = {
radius: 5
};
Możemy obliczyć powierzchnię i obwód koła z jego promienia:
circle.area = function () {
var radius = this.radius;
return Math.PI * radius * radius;
};
circle.circumference = function () {
return 2 * Math.PI * this.radius;
};
Teraz chcę utworzyć kolejny okrąg o promieniu 10
. Jednym ze sposobów na to byłoby:
var circle2 = {
radius: 10,
area: circle.area,
circumference: circle.circumference
};
Jednak JavaScript zapewnia lepszy sposób - delegowanie . Object.create
Funkcja ta jest wykorzystywana w tym celu:
var circle2 = Object.create(circle);
circle2.radius = 10;
To wszystko. Właśnie zrobiłeś prototypowe dziedziczenie w JavaScript. Czy to nie było proste? Bierzesz przedmiot, klonujesz go, zmieniasz wszystko, czego potrzebujesz, i hej presto - masz nowy obiekt.
Teraz możesz zapytać: „Jak to jest proste? Za każdym razem, gdy chcę utworzyć nowy okrąg, muszę go sklonować circle
i ręcznie przypisać mu promień”. Cóż, rozwiązaniem jest użycie funkcji do wykonania ciężkiego podnoszenia:
function createCircle(radius) {
var newCircle = Object.create(circle);
newCircle.radius = radius;
return newCircle;
}
var circle2 = createCircle(10);
W rzeczywistości możesz to wszystko połączyć w jeden dosłowny obiekt w następujący sposób:
var circle = {
radius: 5,
create: function (radius) {
var circle = Object.create(this);
circle.radius = radius;
return circle;
},
area: function () {
var radius = this.radius;
return Math.PI * radius * radius;
},
circumference: function () {
return 2 * Math.PI * this.radius;
}
};
var circle2 = circle.create(10);
Dziedziczenie prototypowe w JavaScript
Jeśli zauważysz w powyższym programie, create
funkcja tworzy klon circle
, przypisuje radius
do niego nowy , a następnie zwraca go. To właśnie robi konstruktor w JavaScript:
function Circle(radius) {
this.radius = radius;
}
Circle.prototype.area = function () {
var radius = this.radius;
return Math.PI * radius * radius;
};
Circle.prototype.circumference = function () {
return 2 * Math.PI * this.radius;
};
var circle = new Circle(5);
var circle2 = new Circle(10);
Wzorzec konstruktora w JavaScript jest odwróconym wzorcem prototypowym. Zamiast tworzyć obiekt, tworzysz konstruktor. new
Kluczowe wiąże this
kursor wewnątrz konstruktora do klonem prototype
konstruktora.
Brzmi myląco? Jest tak, ponieważ wzorzec konstruktora w JavaScript niepotrzebnie komplikuje rzeczy. Trudno to zrozumieć większości programistów.
Zamiast myśleć o obiektach dziedziczących po innych obiektach, myślą o konstruktorach dziedziczących po innych konstruktorach, a następnie stają się całkowicie zdezorientowani.
Istnieje wiele innych powodów, dla których należy unikać wzorca konstruktora w JavaScript. Możesz o nich przeczytać w moim blogu tutaj: Konstruktory kontra prototypy
Jakie są zatem zalety dziedziczenia prototypowego w porównaniu z dziedziczeniem klasycznym? Ponownie przejrzyjmy najczęstsze argumenty i wyjaśnijmy dlaczego .
1. Dziedziczenie prototypowe jest proste
CMS stwierdza w swojej odpowiedzi:
Moim zdaniem główną zaletą dziedziczenia prototypowego jest jego prostota.
Zastanówmy się, co właśnie zrobiliśmy. Stworzyliśmy obiekt circle
o promieniu 5
. Następnie sklonowaliśmy go i nadaliśmy klonowi promień 10
.
Dlatego potrzebujemy tylko dwóch rzeczy, aby dziedziczenie prototypowe działało:
- Sposób na utworzenie nowego obiektu (np. Literały obiektu).
- Sposób rozszerzenia istniejącego obiektu (np
Object.create
.).
Natomiast klasyczne dziedziczenie jest znacznie bardziej skomplikowane. W dziedzictwie klasycznym masz:
- Klasy
- Obiekt.
- Interfejsy
- Klasy abstrakcyjne.
- Klasy końcowe.
- Wirtualne klasy podstawowe.
- Konstruktory
- Niszczyciele
Masz pomysł. Chodzi o to, że dziedziczenie prototypów jest łatwiejsze do zrozumienia, łatwiejsze do wdrożenia i łatwiejsze do uzasadnienia.
Jak pisze Steve Yegge w swoim klasycznym blogu „ Portrait of a N00b ”:
Metadane to dowolny opis lub model czegoś innego. Komentarze w twoim kodzie są po prostu opisem obliczeń w języku naturalnym. Metadane metadanych powodują, że nie są one absolutnie konieczne. Jeśli mam psa z dokumentami rodowodowymi i gubię dokumenty, nadal mam doskonale ważnego psa.
W tym samym sensie klasy są po prostu metadanymi. Klasy nie są ściśle wymagane do dziedziczenia. Jednak niektórzy ludzie (zwykle n00bs) uważają, że zajęcia są wygodniejsze w pracy. Daje im to fałszywe poczucie bezpieczeństwa.
Wiemy również, że typy statyczne to tylko metadane. To specjalistyczny rodzaj komentarza skierowany do dwóch rodzajów czytelników: programistów i kompilatorów. Typy statyczne opowiadają historię o obliczeniach, prawdopodobnie po to, by pomóc obu grupom czytelników zrozumieć intencje programu. Ale typy statyczne można wyrzucić w czasie wykonywania, ponieważ ostatecznie są to tylko stylizowane komentarze. Są jak papierkowa robota: może to sprawić, że pewien niepewny typ osobowości będzie szczęśliwszy z powodu swojego psa, ale psa na pewno to nie obchodzi.
Jak wspomniałem wcześniej, zajęcia dają ludziom fałszywe poczucie bezpieczeństwa. Na przykład NullPointerException
w Javie jest za dużo s, nawet jeśli kod jest doskonale czytelny. Uważam, że klasyczne dziedziczenie zwykle przeszkadza w programowaniu, ale może to tylko Java. Python ma niesamowity klasyczny system dziedziczenia.
2. Dziedziczenie prototypowe jest potężne
Większość programistów wywodzących się z klasycznego pochodzenia twierdzi, że klasyczne dziedziczenie jest potężniejsze niż dziedziczenie prototypowe, ponieważ:
- Zmienne prywatne.
- Wielokrotne dziedziczenie.
To twierdzenie jest fałszywe. Wiemy już, że JavaScript obsługuje zmienne prywatne poprzez zamknięcia , ale co z wielokrotnym dziedziczeniem? Obiekty w JavaScript mają tylko jeden prototyp.
Prawda jest taka, że dziedziczenie prototypów obsługuje dziedziczenie po wielu prototypach. Dziedziczenie prototypowe oznacza po prostu, że jeden obiekt dziedziczy po innym obiekcie. Istnieją dwa sposoby implementacji dziedziczenia prototypowego :
- Delegacja lub dziedziczenie różnicowe
- Klonowanie lub konkatenacyjne dziedziczenie
Tak JavaScript pozwala jedynie na delegowanie obiektów do jednego innego obiektu. Pozwala to jednak skopiować właściwości dowolnej liczby obiektów. Na przykład _.extend
właśnie to robi.
Oczywiście wielu programistów nie uważa tego za dziedzictwo, ponieważ instanceof
i isPrototypeOf
mówią inaczej. Można jednak łatwo temu zaradzić, przechowując tablicę prototypów na każdym obiekcie, który dziedziczy po prototypie przez konkatenację:
function copyOf(object, prototype) {
var prototypes = object.prototypes;
var prototypeOf = Object.isPrototypeOf;
return prototypes.indexOf(prototype) >= 0 ||
prototypes.some(prototypeOf, prototype);
}
Dlatego dziedziczenie prototypowe jest tak samo potężne jak dziedziczenie klasyczne. W rzeczywistości jest znacznie potężniejszy niż klasyczne dziedziczenie, ponieważ w dziedziczeniu prototypowym możesz ręcznie wybrać, które właściwości skopiować, a które pominąć w przypadku różnych prototypów.
W dziedziczeniu klasycznym nie można (lub przynajmniej bardzo trudno) wybrać właściwości, które chcesz dziedziczyć. Używają wirtualnych klas bazowych i interfejsów do rozwiązania problemu diamentów .
Jednak w JavaScript najprawdopodobniej nigdy nie usłyszysz o problemie z diamentem, ponieważ możesz dokładnie kontrolować, które właściwości chcesz dziedziczyć i od których prototypów.
3. Dziedziczenie prototypowe jest mniej zbędne
Ten punkt jest nieco trudniejszy do wyjaśnienia, ponieważ klasyczne dziedziczenie niekoniecznie prowadzi do bardziej zbędnego kodu. W rzeczywistości dziedziczenie, zarówno klasyczne, jak i prototypowe, stosuje się w celu zmniejszenia nadmiarowości kodu.
Jednym z argumentów może być to, że większość języków programowania z klasycznym dziedziczeniem jest typowana statycznie i wymaga od użytkownika jawnego deklarowania typów (w przeciwieństwie do Haskell, który ma niejawne typowanie statyczne). Stąd prowadzi to do bardziej pełnego kodu.
Java jest znana z tego zachowania. Wyraźnie pamiętam Boba Nystroma, który wspomniał o następującej anegdocie w swoim blogu na temat Pratta Parsera :
Musisz pokochać tutaj poziom biurokracji Java: „proszę podpisać go czterokrotnie”.
Ponownie myślę, że dzieje się tak tylko dlatego, że Java jest do bani.
Jednym z ważnych argumentów jest to, że nie wszystkie języki, które mają klasyczne dziedziczenie, obsługują wielokrotne dziedziczenie. Znowu przychodzi mi na myśl Java. Tak Java ma interfejsy, ale to nie wystarczy. Czasami naprawdę potrzebujesz wielokrotnego dziedziczenia.
Ponieważ dziedziczenie prototypowe pozwala na wielokrotne dziedziczenie, kod wymagający wielokrotnego dziedziczenia jest mniej zbędny, jeśli jest napisany przy użyciu dziedziczenia prototypowego, a nie w języku, który ma klasyczne dziedzictwo, ale nie ma wielokrotnego dziedziczenia.
4. Dziedziczenie prototypowe jest dynamiczne
Jedną z najważniejszych zalet dziedziczenia prototypów jest to, że możesz dodawać nowe właściwości do prototypów po ich utworzeniu. Umożliwia to dodawanie nowych metod do prototypu, który zostanie automatycznie udostępniony wszystkim obiektom, które delegują ten prototyp.
Nie jest to możliwe w przypadku klasycznego dziedziczenia, ponieważ po utworzeniu klasy nie można jej modyfikować w czasie wykonywania. Jest to prawdopodobnie największa zaleta dziedziczenia prototypowego w porównaniu z dziedziczeniem klasycznym i powinna być na szczycie. Lubię jednak oszczędzać to, co najlepsze na koniec.
Wniosek
Dziedziczenie prototypowe ma znaczenie. Ważne jest, aby informować programistów JavaScript, dlaczego należy porzucić konstruktorski wzorzec dziedziczenia prototypowego na rzecz prototypowego wzorca dziedziczenia prototypowego.
Musimy zacząć uczyć JavaScript poprawnie, co oznacza pokazanie nowym programistom, jak pisać kod przy użyciu wzorca prototypowego zamiast wzorca konstruktora.
Nie tylko łatwiej będzie wyjaśnić dziedziczenie prototypów za pomocą wzorca prototypowego, ale także sprawi, że będą lepsi programiści.
Jeśli podobała Ci się ta odpowiedź, powinieneś również przeczytać mój post na blogu „ Dlaczego dziedziczenie prototypowe ma znaczenie ”. Zaufaj mi, nie będziesz rozczarowany.