Herencia de Javascript: ¿llamar al superconstructor o usar una cadena de prototipos?

82

Recientemente leí sobre el uso de llamadas de JavaScript en MDC

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

un enlace del ejemplo que se muestra a continuación, todavía no lo entiendo.

¿Por qué están usando la herencia aquí de esta manera?

Prod_dept.prototype = new Product();

¿Es esto necesario? Porque hay una llamada al superconstructor en

Prod_dept()

de todos modos, así

Product.call

¿Es esto simplemente un comportamiento común? ¿Cuándo es mejor usar call para el superconstructor o usar la cadena de prototipos?

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

Gracias por aclarar las cosas

Jeremy S.
fuente
La forma en que lo usó es casi correcta, pero es posible que desee usar Object.create () en lugar de instanciar la base usando la nueva palabra clave (podría causar problemas si el constructor de la base necesita argumentos). Tengo detalles en mi blog: ncombo.wordpress.com/2013/07/11/…
Jon
2
También tenga en cuenta que Product () se llama efectivamente dos veces.
event_jr

Respuestas:

109

La respuesta a la pregunta real es que debes hacer ambas cosas:

  • Establecer el prototipo a una instancia del padre inicializa la cadena del prototipo (herencia), esto se hace solo una vez (ya que el objeto prototipo es compartido).
  • Llamar al constructor del padre inicializa el objeto en sí, esto se hace con cada instanciación (puede pasar diferentes parámetros cada vez que lo construye).

Por lo tanto, no debe llamar al constructor del padre al configurar la herencia. Solo al crear una instancia de un objeto que hereda de otro.

La respuesta de Chris Morgan está casi completa, falta un pequeño detalle (propiedad del constructor). Permítanme sugerir un método para configurar la herencia.

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

Vea la publicación de mi blog para obtener aún más azúcar sintáctico al crear clases. http://js-bits.blogspot.com/2010/08/javascript-inheritance-done-right.html

Técnica copiada de Ext-JS y http://www.uselesspickles.com/class_library/ y un comentario de https://stackoverflow.com/users/1397311/ccnokes

Juan Mendes
fuente
6
En EcmaScript5 + (todos los navegadores modernos), puede convertirlo en no enumerable si lo define así. De esta Object.defineProperty(sub.protoype, 'constructor', { enumerable: false, value: sub }); forma obtendrá exactamente el mismo "comportamiento" que cuando javascript crea una nueva instancia de una función (el constructor se establece como enumerable = falso automáticamente)
Adaptabi
2
¿No podría simplemente simplificar el método de extensión a dos líneas? A saber: sub.prototype = Object.create (base.prototype); sub.prototype.constructor = sub;
Apertura el
@Steve Sí, cuando escribí esto por primera vez, Object.createno estaba tan bien apoyado ... actualizarlo. Tenga en cuenta que la mayoría de los Object.createpolyfills se implementan utilizando la técnica que mostré originalmente.
Juan Mendes
1
Entonces, si quisiera agregar métodos solo al objeto secundario y sus instancias, el objeto "Perro" en este caso, ¿fusionaría los dos prototipos dentro de su función de extensión de esta manera: jsfiddle.net/ccnokes/75f9P ?
ccnokes
1
@ elad.chen El enfoque que he descrito en la respuesta encadena el prototipo, los mixins generalmente copian todas las propiedades en una instancia, no en el prototipo. Ver stackoverflow.com/questions/7506210/…
Juan Mendes
30

La forma ideal de hacerlo es no hacerlo Prod_dept.prototype = new Product();, porque este llama al Productconstructor. Entonces, la forma ideal es clonarlo, excepto el constructor, algo como esto:

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;

Luego, se llama al superconstructor en el momento de la construcción, que es lo que desea, porque entonces también puede pasar los parámetros.

Si observa cosas como la Biblioteca de cierre de Google, verá que así es como lo hacen.

Chris Morgan
fuente
Llamo al constructor utilizado para configurar la herencia, el constructor sustituto. Su ejemplo aún se olvida de restablecer la propiedad del constructor después de configurar la herencia para que pueda detectar correctamente el constructor
Juan Mendes
1
@Juan: OK, actualizado para agregar Prod_dept.prototype.constructor = Prod_dept;.
Chris Morgan
@ChrisMorgan estoy teniendo problemas para entender la última línea de la muestra: Prod_dept.prototype.constructor = Prod_dept;. En primer lugar, ¿por qué es necesario y por qué señala en Prod_deptlugar de Product?
Lasse Christiansen
1
@ LasseChristiansen-sw_lasse: Prod_dept.prototypees lo que se utilizará como prototipo de la salida de new Prod_dept(). (Por lo general, ese prototipo está disponible como instance.__proto__, aunque esto es un detalle de implementación). En cuanto a por qué constructor, es una parte estándar del lenguaje y, por lo tanto, debe proporcionarse para mantener la coherencia; es correcto por defecto, pero debido a que estamos reemplazando el prototipo por completo, debemos asignar el valor correcto nuevamente, o algunas cosas no estarán sanas (en este caso, significaría que una Prod_deptinstancia lo habría hecho this.constructor == Product, lo cual es malo).
Chris Morgan
6

Si ha realizado Programación Orientada a Objetos en JavaScript, sabrá que puede crear una clase de la siguiente manera:

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

Hasta ahora nuestra persona de clase solo tiene dos propiedades y le vamos a dar algunos métodos. Una forma limpia de hacer esto es usar su objeto 'prototipo'. A partir de JavaScript 1.1, el objeto prototipo se introdujo en JavaScript. Este es un objeto integrado que simplifica el proceso de agregar propiedades y métodos personalizados a todas las instancias de un objeto. Agreguemos 2 métodos a nuestra clase usando su objeto 'prototipo' de la siguiente manera:

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

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

Ahora hemos definido nuestra clase Person. ¿Y si quisiéramos definir otra clase llamada Manager que hereda algunas propiedades de Person? No tiene sentido volver a definir todas estas propiedades cuando definamos nuestra clase Manager, podemos configurarla para que herede de la clase Person. JavaScript no tiene herencia incorporada, pero podemos usar una técnica para implementar la herencia de la siguiente manera:

Inheritance_Manager = {};// Creamos una clase de administrador de herencia (el nombre es arbitrario)

Ahora démosle a nuestra clase de herencia un método llamado extender que toma los argumentos baseClass y subClassas. Dentro del método extend, crearemos una clase interna llamada función de herencia herencia () {}. La razón por la que usamos esta clase interna es para evitar confusiones entre los prototipos baseClass y subClass. A continuación, hacemos que el prototipo de nuestra clase de herencia apunte al prototipo baseClass como con el siguiente código: heritage.prototype = baseClass. prototipo; Luego copiamos el prototipo de herencia en el prototipo de subclase de la siguiente manera: subClass.prototype = nueva herencia (); Lo siguiente es especificar el constructor para nuestra subclase de la siguiente manera: subClass.prototype.constructor = subClass; Una vez que terminamos con la creación de prototipos de subclase, podemos especificar las siguientes dos líneas de código para establecer algunos punteros de clase base.

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

Aquí está el código completo para nuestra función de extensión:

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

Ahora que hemos implementado nuestra herencia, podemos comenzar a usarla para extender nuestras clases. En este caso, vamos a extender nuestra clase Person a una clase Manager de la siguiente manera:

Definimos la clase Manager

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

lo hacemos heredar de la persona

Inheritance_Manager.extend(Manager, Person);

Si se dio cuenta, acabamos de llamar al método extend de nuestra clase Inheritance_Manager y pasamos la subClass Manager en nuestro caso y luego la baseClass Person. Tenga en cuenta que el orden es muy importante aquí. Si los intercambia, la herencia no funcionará como pretendía. También tenga en cuenta que deberá especificar esta herencia antes de poder definir nuestra subclase. Ahora definamos nuestra subclase:

Podemos agregar más métodos como el siguiente. Nuestra clase Manager siempre tendrá los métodos y propiedades definidos en la clase Person porque hereda de ella.

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

Ahora para probarlo, creemos dos objetos, uno de la clase Person y otro de la clase heredada Manager:

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

No dude en obtener el código completo y más comentarios en: http://www.cyberminds.co.uk/blog/articles/how-to-implement-javascript-inheritance.aspx

Joe Francis
fuente