Dziedziczenie JavaScript: wywołanie super-konstruktora lub użycie łańcucha prototypów?

82

Całkiem niedawno czytałem o używaniu wywołań JavaScript w MDC

https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Function/call

jeden link z przykładu pokazanego poniżej, nadal nie rozumiem.

Dlaczego używają tutaj dziedziczenia w ten sposób

Prod_dept.prototype = new Product();

czy to konieczne? Ponieważ istnieje wywołanie superkonstruktora w programie

Prod_dept()

w każdym razie w ten sposób

Product.call

czy to po prostu nietypowe zachowanie? Kiedy lepiej jest użyć wywołania dla superkonstruktora lub łańcucha prototypów?

function Product(name, value){
  this.name = name;
  if(value >= 1000)
    this.value = 999;
  else
    this.value = value;
}

function Prod_dept(name, value, dept){
  this.dept = dept;
  Product.call(this, name, value);
}

Prod_dept.prototype = new Product();

// since 5 is less than 1000, value is set
cheese = new Prod_dept("feta", 5, "food");

// since 5000 is above 1000, value will be 999
car = new Prod_dept("honda", 5000, "auto");

Dzięki za wyjaśnienie sprawy

Jeremy S.
źródło
Sposób, w jaki go użyłeś, jest prawie właściwy, ale możesz chcieć użyć Object.create () zamiast tworzenia wystąpienia bazy za pomocą słowa kluczowego new (może to spowodować problemy, jeśli konstruktor podstawowy potrzebuje argumentów). Mam szczegóły na moim blogu: ncombo.wordpress.com/2013/07/11/…
Jon
2
Zwróć również uwagę, że Product () jest skutecznie wywoływana dwukrotnie.
event_jr

Odpowiedzi:

109

Odpowiedź na prawdziwe pytanie jest taka, że ​​musisz zrobić jedno i drugie:

  • Ustawienie prototypu na instancję rodzica inicjuje łańcuch prototypów (dziedziczenie), robi się to tylko raz (ponieważ obiekt prototypu jest współdzielony).
  • Wywołanie konstruktora rodzica inicjuje sam obiekt, odbywa się to przy każdej instancji (możesz przekazać różne parametry za każdym razem, gdy go konstruujesz).

Dlatego podczas konfigurowania dziedziczenia nie należy wywoływać konstruktora elementu nadrzędnego. Tylko podczas tworzenia wystąpienia obiektu, który dziedziczy po innym.

Odpowiedź Chrisa Morgana jest prawie kompletna, brakuje jej małego szczegółu (właściwość konstruktora). Zasugeruję metodę konfigurowania dziedziczenia.

function extend(base, sub) {
  // Avoid instantiating the base class just to setup inheritance
  // Also, do a recursive merge of two prototypes, so we don't overwrite 
  // the existing prototype, but still maintain the inheritance chain
  // Thanks to @ccnokes
  var origProto = sub.prototype;
  sub.prototype = Object.create(base.prototype);
  for (var key in origProto)  {
     sub.prototype[key] = origProto[key];
  }
  // The constructor property was set wrong, let's fix it
  Object.defineProperty(sub.prototype, 'constructor', { 
    enumerable: false, 
    value: sub 
  });
}

// Let's try this
function Animal(name) {
  this.name = name;
}

Animal.prototype = {
  sayMyName: function() {
    console.log(this.getWordsToSay() + " " + this.name);
  },
  getWordsToSay: function() {
    // Abstract
  }
}

function Dog(name) {
  // Call the parent's constructor
  Animal.call(this, name);
}

Dog.prototype = {
    getWordsToSay: function(){
      return "Ruff Ruff";
    }
}    

// Setup the prototype chain the right way
extend(Animal, Dog);

// Here is where the Dog (and Animal) constructors are called
var dog = new Dog("Lassie");
dog.sayMyName(); // Outputs Ruff Ruff Lassie
console.log(dog instanceof Animal); // true
console.log(dog.constructor); // Dog

Zobacz mój wpis na blogu, aby uzyskać jeszcze więcej cukru składniowego podczas tworzenia zajęć. http://js-bits.blogspot.com/2010/08/javascript-inheritance-done-right.html

Technika skopiowana z Ext-JS i http://www.uselesspickles.com/class_library/ oraz komentarz z https://stackoverflow.com/users/1397311/ccnokes

Juan Mendes
źródło
6
W EcmaScript5 + (we wszystkich nowoczesnych przeglądarkach) możesz ustawić to jako niepoliczalne jeśli zdefiniujesz to w Object.defineProperty(sub.protoype, 'constructor', { enumerable: false, value: sub }); ten sposób W ten sposób uzyskasz dokładnie to samo "zachowanie" jak gdy javascript tworzy nowe wystąpienie funkcji (konstruktor jest ustawiony jako wyliczalny = false automatycznie)
Adaptabi
2
Czy nie mógłbyś po prostu uprościć metody rozszerzania do dwóch linii? Mianowicie: sub.prototype = Object.create (base.prototype); sub.prototype.constructor = sub;
Appetere
@Steve Tak, możesz, kiedy pierwszy raz to napisałem, Object.createnie było tak dobrze wspierane ... aktualizacja. Zauważ, że większość Object.createpolyfillów jest realizowana przy użyciu techniki, którą pokazałem pierwotnie.
Juan Mendes
1
Więc gdybym chciał dodać metody tylko do obiektu podrzędnego i jego instancji, w tym przypadku obiektu „Dog”, czy po prostu scaliłbyś dwa prototypy w ramach funkcji rozszerzającej w następujący sposób: jsfiddle.net/ccnokes/75f9P ?
ccnokes
1
@ elad.chen Podejście, które opisałem w odpowiedziach, łączy w łańcuch prototyp, miksy zwykle kopiują wszystkie właściwości do instancji, a nie do prototypu. Zobacz stackoverflow.com/questions/7506210/…
Juan Mendes
30

Idealnym sposobem na to jest nie robienie tego Prod_dept.prototype = new Product();, ponieważ wywołuje to Productkonstruktora. Więc idealnym sposobem jest sklonowanie go z wyjątkiem konstruktora, coś takiego:

function Product(...) {
    ...
}
var tmp = function(){};
tmp.prototype = Product.prototype;

function Prod_dept(...) {
    Product.call(this, ...);
}
Prod_dept.prototype = new tmp();
Prod_dept.prototype.constructor = Prod_dept;

Następnie w czasie budowy wywoływany jest superkonstruktor, co jest tym, czego chcesz, ponieważ wtedy również możesz przekazać parametry.

Jeśli spojrzysz na takie rzeczy, jak Google Closure Library, zobaczysz, jak to robią.

Chris Morgan
źródło
Nazywam tego konstruktora używanego do konfigurowania dziedziczenia konstruktorem zastępczym. Twój przykład wciąż zapomina o zresetowaniu właściwości konstruktora po skonfigurowaniu dziedziczenia, aby można było poprawnie wykryć konstruktor
Juan Mendes
1
@Juan: OK, zaktualizowano, aby dodać Prod_dept.prototype.constructor = Prod_dept;.
Chris Morgan,
@ChrisMorgan mam problemy ze zrozumieniem ostatni wiersz w próbce: Prod_dept.prototype.constructor = Prod_dept;. Po pierwsze, dlaczego jest potrzebny i dlaczego Prod_deptzamiast tego wskazuje Product?
Lasse Christiansen,
1
@ LasseChristiansen-sw_lasse: Prod_dept.prototypejest tym, co zostanie użyte jako prototyp wyniku new Prod_dept(). (Zwykle ten prototyp jest dostępny tak instance.__proto__, jakby był to szczegół implementacji). A jeśli chodzi o powód constructor- jest to standardowa część języka i dlatego powinna być zapewniona dla spójności; Domyślnie jest to poprawne, ale ponieważ całkowicie zastępujemy prototyp, musimy ponownie przypisać odpowiednią wartość, w przeciwnym razie niektóre rzeczy nie będą rozsądne (w tym przypadku oznaczałoby to, że wystąpiłaby Prod_deptinstancja this.constructor == Product, co jest złe).
Chris Morgan,
6

Jeśli zrobiłeś już programowanie obiektowe w JavaScript, wiesz, że możesz utworzyć klasę w następujący sposób:

Person = function(id, name, age){
    this.id = id;
    this.name = name;
    this.age = age;
    alert('A new person has been accepted');
}

Jak dotąd osoba z klasy ma tylko dwie właściwości i zamierzamy podać jej kilka metod. Prostym sposobem na to jest użycie obiektu „prototypu”. Począwszy od JavaScript 1.1, obiekt prototypowy został wprowadzony w JavaScript. Jest to obiekt wbudowany, który upraszcza proces dodawania niestandardowych właściwości i metod do wszystkich wystąpień obiektu. Dodajmy 2 metody do naszej klasy, używając jej obiektu „prototyp” w następujący sposób:

Person.prototype = {
    /** wake person up */
    wake_up: function() {
        alert('I am awake');
    },

    /** retrieve person's age */
    get_age: function() {
        return this.age;
    }
}

Teraz zdefiniowaliśmy naszą klasę Person. A co by było, gdybyśmy chcieli zdefiniować inną klasę o nazwie Manager, która dziedziczy niektóre właściwości z Person. Nie ma sensu ponownie definiować wszystkich tych właściwości, kiedy definiujemy naszą klasę Manager, możemy po prostu ustawić ją tak, aby dziedziczyła z klasy Person. JavaScript nie ma wbudowanego dziedziczenia, ale możemy użyć następującej techniki do zaimplementowania dziedziczenia:

Inheritance_Manager = {};// Tworzymy klasę menedżera dziedziczenia (nazwa jest dowolna)

Teraz dajmy naszej klasie dziedziczenia metodę o nazwie extension, która przyjmuje argumenty baseClass i subClassas. W ramach metody rozszerzania utworzymy klasę wewnętrzną o nazwie inheritance function inheritance () {}. Powodem, dla którego używamy tej klasy wewnętrznej, jest uniknięcie pomyłki między prototypami baseClass i subClass. Następnie sprawiamy, że prototyp naszej klasy dziedziczenia wskazuje na prototyp baseClass, tak jak w następującym kodzie: inheritance.prototype = baseClass. prototyp; Następnie kopiujemy prototyp dziedziczenia do prototypu podklasy w następujący sposób: subClass.prototype = new inheritance (); Następną rzeczą jest określenie konstruktora dla naszej podklasy w następujący sposób: subClass.prototype.constructor = subClass; Po zakończeniu naszego prototypowania podklasy możemy określić następne dwa wiersze kodu, aby ustawić wskaźniki klasy bazowej.

subClass.baseConstructor = baseClass;
subClass.superClass = baseClass.prototype;

Oto pełny kod naszej funkcji rozszerzającej:

Inheritance_Manager.extend = function(subClass, baseClass) {
    function inheritance() { }
    inheritance.prototype = baseClass.prototype;
    subClass.prototype = new inheritance();
    subClass.prototype.constructor = subClass;
    subClass.baseConstructor = baseClass;
    subClass.superClass = baseClass.prototype;
}

Teraz, gdy zaimplementowaliśmy nasze dziedziczenie, możemy zacząć używać go do rozszerzania naszych klas. W tym przypadku zamierzamy rozszerzyć naszą klasę Person na klasę Manager w następujący sposób:

Definiujemy klasę Manager

Manager = function(id, name, age, salary) {
    Person.baseConstructor.call(this, id, name, age);
    this.salary = salary;
    alert('A manager has been registered.');
}

sprawiamy, że dziedziczymy po osobie

Inheritance_Manager.extend(Manager, Person);

Jeśli zauważyłeś, właśnie wywołaliśmy metodę rozszerzoną naszej klasy Inheritance_Manager i przekazaliśmy Menedżera podklasy w naszym przypadku, a następnie osobę baseClass. Zwróć uwagę, że kolejność jest tutaj bardzo ważna. Jeśli je zamienisz, dziedziczenie nie będzie działać tak, jak zamierzałeś, jeśli w ogóle. Zauważ również, że będziesz musiał określić to dziedziczenie, zanim będziesz mógł faktycznie zdefiniować naszą podklasę. Teraz zdefiniujmy naszą podklasę:

Możemy dodać więcej metod, jak ta poniżej. Nasza klasa Manager zawsze będzie miała metody i właściwości zdefiniowane w klasie Person, ponieważ dziedziczy po niej.

Manager.prototype.lead = function(){
   alert('I am a good leader');
}

Aby to przetestować, stwórzmy dwa obiekty, jeden z klasy Person i jeden z odziedziczonej klasy Manager:

var p = new Person(1, 'Joe Tester', 26);
var pm = new Manager(1, 'Joe Tester', 26, '20.000');

Zapraszam do uzyskania pełnego kodu i dodatkowych komentarzy pod adresem : http://www.cyberminds.co.uk/blog/articles/how-to-implement-javascript-inheritance.aspx

Joe Francis
źródło