Наследование javascript: вызвать суперконструктор или использовать цепочку прототипов?

82

Совсем недавно я прочитал об использовании вызовов JavaScript в MDC.

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

одну ссылку на пример, показанный ниже, я все еще не понимаю.

Почему они используют наследование вот так

Prod_dept.prototype = new Product();

это необходимо? Потому что есть вызов суперконструктора в

Prod_dept()

во всяком случае, вот так

Product.call

это просто необычное поведение? Когда лучше использовать вызов супер-конструктора или использовать цепочку прототипов?

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");

Спасибо за разъяснение

Джереми С.
источник
То, как вы его использовали, почти правильно, но вы можете использовать Object.create () вместо создания экземпляра базы с помощью ключевого слова new (может вызвать проблемы, если базовому конструктору нужны аргументы). У меня есть подробности в моем блоге: ncombo.wordpress.com/2013/07/11/…
Джон
2
Также обратите внимание, что Product () фактически вызывается дважды.
event_jr 03

Ответы:

109

Ответ на настоящий вопрос заключается в том, что вам нужно сделать и то, и другое:

  • Установка прототипа на экземпляр родителя инициализирует цепочку прототипов (наследование), это делается только один раз (поскольку объект прототипа является общим).
  • Вызов родительского конструктора инициализирует сам объект, это делается при каждом создании экземпляра (вы можете передавать разные параметры каждый раз, когда вы его создаете).

Поэтому не следует вызывать родительский конструктор при настройке наследования. Только при создании экземпляра объекта, наследуемого от другого.

Ответ Криса Моргана почти полный, отсутствует небольшая деталь (свойство конструктора). Позвольте предложить способ настройки наследования.

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

См. Мое сообщение в блоге для получения дополнительной информации о синтаксическом сахаре при создании классов. http://js-bits.blogspot.com/2010/08/javascript-inheritance-done-right.html

Техника скопирована из Ext-JS и http://www.uselesspickles.com/class_library/ и комментарий из https://stackoverflow.com/users/1397311/ccnokes

Хуан Мендес
источник
6
В EcmaScript5 + (все современные браузеры) вы можете сделать его неперечисляемым, если определите его таким образом. Object.defineProperty(sub.protoype, 'constructor', { enumerable: false, value: sub }); Таким образом вы получите точно такое же «поведение», как когда javascript создает новый экземпляр функции (конструктор установлен как enumerable = false автоматически)
Adaptabi
2
Не могли бы вы просто упростить метод расширения до двух строк? А именно: sub.prototype = Object.create (base.prototype); sub.prototype.constructor = sub;
Appetere
@Steve: Да, вы можете, когда я впервые написал это, поддержка Object.createне была такой хорошей ... обновление. Обратите внимание, что большинство Object.createполифиллов реализовано с использованием техники, которую я показал изначально.
Хуан Мендес
1
Итак, если бы я хотел добавить методы только к дочернему объекту и его экземплярам, ​​в данном случае к объекту «Dog», не могли бы вы просто объединить два прототипа в своей расширенной функции следующим образом: jsfiddle.net/ccnokes/75f9P ?
ccnokes 01
1
@ elad.chen Подход, который я описал в цепочках ответов прототипа, миксины обычно копируют все свойства в экземпляр, а не в прототип. См. Stackoverflow.com/questions/7506210/…
Хуан Мендес
30

Идеальный способ сделать это - не делать Prod_dept.prototype = new Product();, потому что это вызывает Productконструктор. Итак, идеальный способ - клонировать его, за исключением конструктора, примерно так:

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;

Затем во время построения вызывается суперконструктор, что вам и нужно, потому что тогда вы также можете передавать параметры.

Если вы посмотрите на такие вещи, как Google Closure Library, вы увидите, как они это делают.

Крис Морган
источник
Я называю этот конструктор, используемый для настройки наследования, суррогатным конструктором. В вашем примере все еще забывается сбросить свойство конструктора после настройки наследования, чтобы вы могли правильно определить конструктор
Хуан Мендес,
1
@Juan: Хорошо, обновлено, чтобы добавить Prod_dept.prototype.constructor = Prod_dept;.
Крис Морган
@ChrisMorgan У меня возникли проблемы с пониманием последней строки в образце: Prod_dept.prototype.constructor = Prod_dept;. Прежде всего, зачем это нужно и почему Prod_deptвместо Product?
Лассе Кристиансен
1
@ LasseChristiansen-sw_lasse: Prod_dept.prototypeэто то, что будет использоваться в качестве прототипа выходных данных new Prod_dept(). (Обычно этот прототип доступен как instance.__proto__, хотя это деталь реализации.) Что касается того, почему constructor- это стандартная часть языка и поэтому должна быть предоставлена ​​для единообразия; это правильно по умолчанию, но поскольку мы полностью заменяем прототип, мы должны снова присвоить правильное значение, иначе некоторые вещи будут не в порядке (в этом случае это будет означать, что Prod_deptэкземпляр будет иметь this.constructor == Product, что плохо).
Крис Морган
6

Если вы занимались объектно-ориентированным программированием на JavaScript, вы знаете, что можете создать класс следующим образом:

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

Пока что у нашего класса person только два свойства, и мы собираемся дать ему несколько методов. Чистый способ сделать это - использовать его объект-прототип. Начиная с JavaScript 1.1, объект-прототип был введен в JavaScript. Это встроенный объект, который упрощает процесс добавления настраиваемых свойств и методов ко всем экземплярам объекта. Давайте добавим к нашему классу 2 метода, используя его объект-прототип следующим образом:

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

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

Теперь мы определили наш класс Person. Что, если бы мы хотели определить другой класс с именем Manager, который наследует некоторые свойства от Person. Нет смысла заново определять все эти свойства, когда мы определяем наш класс Manager, мы можем просто установить его для наследования от класса Person. JavaScript не имеет встроенного наследования, но мы можем использовать следующий метод для реализации наследования:

Inheritance_Manager = {};// Создаем класс менеджера наследования (название произвольное)

Теперь давайте дадим нашему классу наследования метод под названием extend, который принимает аргументы baseClass и subClassas. Внутри метода расширения мы создадим внутренний класс, называемый функцией наследования inheritance () {}. Причина, по которой мы используем этот внутренний класс, состоит в том, чтобы избежать путаницы между прототипами baseClass и subClass. Затем мы заставляем прототип нашего класса наследования указывать на прототип baseClass, как в следующем коде: inheritance.prototype = baseClass. опытный образец; Затем мы копируем прототип наследования в прототип подкласса следующим образом: subClass.prototype = new inheritance (); Следующее, что нужно сделать, - это указать конструктор для нашего подкласса следующим образом: subClass.prototype.constructor = subClass; Закончив прототипирование нашего подкласса, мы можем указать следующие две строки кода, чтобы установить некоторые указатели базового класса.

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

Вот полный код нашей функции расширения:

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;
}

Теперь, когда мы реализовали наше наследование, мы можем начать использовать его для расширения наших классов. В этом случае мы собираемся расширить наш класс Person до класса Manager следующим образом:

Определяем класс Manager

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

мы наследуем от человека

Inheritance_Manager.extend(Manager, Person);

Если вы заметили, мы только что вызвали метод расширения нашего класса Inheritance_Manager и в нашем случае передали диспетчер подклассов, а затем - человека базового класса. Обратите внимание, что порядок здесь очень важен. Если вы поменяете их местами, наследование не будет работать так, как вы планировали, если вообще. Также обратите внимание, что вам нужно будет указать это наследование, прежде чем вы действительно сможете определить наш подкласс. Теперь давайте определим наш подкласс:

Мы можем добавить больше методов, как показано ниже. Наш класс Manager всегда будет иметь методы и свойства, определенные в классе Person, потому что он наследуется от него.

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

Теперь, чтобы проверить это, давайте создадим два объекта, один из класса Person и один из унаследованного класса Manager:

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

Не стесняйтесь получить полный код и другие комментарии по адресу: http://www.cyberminds.co.uk/blog/articles/how-to-implement-javascript-inheritance.aspx

Джо Фрэнсис
источник