Buen ejemplo de herencia basada en prototipos de JavaScript

89

He programado con lenguajes de programación orientada a objetos durante más de 10 años, pero ahora estoy aprendiendo JavaScript y es la primera vez que me encuentro con la herencia basada en prototipos. Tiendo a aprender más rápido al estudiar un buen código. ¿Qué es un ejemplo bien escrito de una aplicación (o biblioteca) de JavaScript que utiliza correctamente la herencia de prototipos? ¿Y puede describir (brevemente) cómo / dónde se usa la herencia prototípica, para saber por dónde empezar a leer?

Alex Reisner
fuente
1
¿Tuviste la oportunidad de consultar esa biblioteca Base? Realmente es bonito y bastante pequeño. Si le gusta, considere marcar mi respuesta como la respuesta. TIA, roland.
Roland Bouman
Supongo que estoy en el mismo barco que tú. Quiero, también, aprender un poco sobre este lenguaje prototípico, no estar restringido solo a frameworks oop o similares, aunque sean geniales y todo, necesitamos aprender, ¿verdad? No solo un marco hace eso por mí, incluso si lo voy a usar. Pero aprenda a crear cosas nuevas en nuevos lenguajes con nuevas formas, piense fuera de la caja. Me gusta tu estilo. Voy a intentar ayudarme y tal vez ayudarte. Tan pronto como encuentre algo, te lo haré saber.
marcelo-ferraz

Respuestas:

48

Douglas Crockford tiene una bonita página sobre herencia de prototipos de JavaScript :

Hace cinco años escribí Herencia clásica en JavaScript. Demostró que JavaScript es un lenguaje prototípico sin clases y que tiene suficiente poder expresivo para simular un sistema clásico. Mi estilo de programación ha evolucionado desde entonces, como debería hacerlo cualquier buen programador. He aprendido a abrazar plenamente el prototipo y me he liberado de los confines del modelo clásico.

Base.js de Dean Edward , la clase de Mootools o las obras de herencia simple de John Resig son formas de hacer herencia clásica en JavaScript.

Gregory Pakosz
fuente
¿Por qué no simplemente newObj = Object.create(oldObj);si lo quieres sin clases? De lo contrario, ¿ oldObjdebería funcionar reemplazar con el objeto prototipo de la función constructora?
Cyker
76

Como se mencionó, las películas de Douglas Crockford dan una buena explicación sobre el por qué y cubren el cómo. Pero para ponerlo en un par de líneas de JavaScript:

// Declaring our Animal object
var Animal = function () {

    this.name = 'unknown';

    this.getName = function () {
        return this.name;
    }

    return this;
};

// Declaring our Dog object
var Dog = function () {

    // A private variable here        
    var private = 42;

    // overriding the name
    this.name = "Bello";

    // Implementing ".bark()"
    this.bark = function () {
        return 'MEOW';
    }  

    return this;
};


// Dog extends animal
Dog.prototype = new Animal();

// -- Done declaring --

// Creating an instance of Dog.
var dog = new Dog();

// Proving our case
console.log(
    "Is dog an instance of Dog? ", dog instanceof Dog, "\n",
    "Is dog an instance of Animal? ", dog instanceof Animal, "\n",
    dog.bark() +"\n", // Should be: "MEOW"
    dog.getName() +"\n", // Should be: "Bello"
    dog.private +"\n" // Should be: 'undefined'
);

Sin embargo, el problema con este enfoque es que volverá a crear el objeto cada vez que cree uno. Otro enfoque es declarar sus objetos en la pila de prototipos, así:

// Defining test one, prototypal
var testOne = function () {};
testOne.prototype = (function () {
    var me = {}, privateVariable = 42;
    me.someMethod = function () {
        return privateVariable;
    };

    me.publicVariable = "foo bar";
    me.anotherMethod = function () {
        return this.publicVariable;
    };

    return me;

}());


// Defining test two, function
var testTwo = function() {
    var me = {}, privateVariable = 42;
    me.someMethod = function () {
        return privateVariable;
    };

    me.publicVariable = "foo bar";
    me.anotherMethod = function () {
        return this.publicVariable;
    };

    return me;
};


// Proving that both techniques are functionally identical
var resultTestOne = new testOne(),
    resultTestTwo = new testTwo();

console.log(
    resultTestOne.someMethod(), // Should print 42
    resultTestOne.publicVariable // Should print "foo bar"
);

console.log(
    resultTestTwo.someMethod(), // Should print 42
    resultTestTwo.publicVariable // Should print "foo bar"
);



// Performance benchmark start
var stop, start, loopCount = 1000000;

// Running testOne
start = (new Date()).getTime(); 
for (var i = loopCount; i>0; i--) {
    new testOne();
}
stop = (new Date()).getTime();

console.log('Test one took: '+ Math.round(((stop/1000) - (start/1000))*1000) +' milliseconds');



// Running testTwo
start = (new Date()).getTime(); 
for (var i = loopCount; i>0; i--) {
    new testTwo();
}
stop = (new Date()).getTime();

console.log('Test two took: '+ Math.round(((stop/1000) - (start/1000))*1000) +' milliseconds');

Hay un pequeño inconveniente en lo que respecta a la introspección. El volcado de testOne resultará en información menos útil. También la propiedad privada "privateVariable" en "testOne" se comparte en todos los casos, como se menciona en las respuestas de shesek.

Dynom
fuente
3
Tenga en cuenta que en testOne privateVariablees simplemente una variable en el alcance del IIFE y se comparte en todas las instancias, por lo que no debe almacenar datos específicos de la instancia en ella. (en testTwo es específico de la instancia, ya que cada llamada a testTwo () crea un nuevo alcance por instancia)
shesek
Voté a favor porque mostraste el otro enfoque y por qué no usarlo porque hace copias
Murphy316
El problema de recrear el objeto cada vez se debe principalmente a que los métodos se recrean para cada nuevo objeto. Sin embargo, podemos mitigar el problema definiendo el método en Dog.prototype. Entonces, en lugar de usar this.bark = function () {...}, podemos hacerlo Dot.prototype.bark = function () {...}fuera de la Dogfunción. (Ver más detalles en esta respuesta )
Huang Chao
26
function Shape(x, y) {
    this.x = x;
    this.y = y;
}

// 1. Explicitly call base (Shape) constructor from subclass (Circle) constructor passing this as the explicit receiver
function Circle(x, y, r) {
    Shape.call(this, x, y);
    this.r = r;
}

// 2. Use Object.create to construct the subclass prototype object to avoid calling the base constructor
Circle.prototype = Object.create(Shape.prototype);
Vlad Bezden
fuente
3
Quizás agregar este enlace con su respuesta podría completar la imagen aún más: developer.mozilla.org/en/docs/Web/JavaScript/Reference/…
Dynom
14

Echaría un vistazo a YUI y a la Basebiblioteca de Dean Edward : http://dean.edwards.name/weblog/2006/03/base/

Para YUI, puede echar un vistazo rápido al módulo lang , esp. el método YAHOO.lang.extend . Y luego, puede buscar la fuente de algunos widgets o utilidades y ver cómo usan ese método.

Roland Bouman
fuente
YUI 2 ha quedado obsoleto a partir de 2011, por lo que el enlace a langestá medio roto. ¿Alguien quiere arreglarlo para YUI 3?
ack el
lang en yui 3 no parece tener un método de extensión. pero como la respuesta tiene la intención de usar la implementación como ejemplo, la versión no importa.
eMBee
4

Este es el ejemplo más claro que he encontrado, del libro Node de Mixu ( http://book.mixu.net/node/ch6.html ):

Prefiero la composición sobre la herencia:

Composición: la funcionalidad de un objeto se compone de un agregado de diferentes clases que contiene instancias de otros objetos. Herencia: la funcionalidad de un objeto se compone de su propia funcionalidad más la funcionalidad de sus clases principales. Si debe tener herencia, use JS simple y antiguo

Si debe implementar la herencia, al menos evite usar otra implementación / función mágica no estándar. Así es como puede implementar un facsímil razonable de herencia en ES3 puro (siempre que siga la regla de nunca definir propiedades en prototipos):

function Animal(name) {
  this.name = name;
};
Animal.prototype.move = function(meters) {
  console.log(this.name+" moved "+meters+"m.");
};

function Snake() {
  Animal.apply(this, Array.prototype.slice.call(arguments));
};
Snake.prototype = new Animal();
Snake.prototype.move = function() {
  console.log("Slithering...");
  Animal.prototype.move.call(this, 5);
};

var sam = new Snake("Sammy the Python");
sam.move();

Esto no es lo mismo que la herencia clásica, pero es un Javascript estándar y comprensible y tiene la funcionalidad que la gente busca principalmente: constructores encadenables y la capacidad de llamar a métodos de la superclase.

superhombre
fuente
4

ES6 classyextends

ES6 classy extendsson solo azúcar de sintaxis para la manipulación de la cadena de prototipos previamente posible y, por lo tanto, posiblemente la configuración más canónica.

Primero, obtenga más información sobre la .búsqueda de propiedades y cadenas de prototipos en: https://stackoverflow.com/a/23877420/895245

Ahora deconstruyamos lo que sucede:

class C {
    constructor(i) {
        this.i = i
    }
    inc() {
        return this.i + 1
    }
}

class D extends C {
    constructor(i) {
        super(i)
    }
    inc2() {
        return this.i + 2
    }
}
// Inheritance syntax works as expected.
(new C(1)).inc() === 2
(new D(1)).inc() === 2
(new D(1)).inc2() === 3
// "Classes" are just function objects.
C.constructor === Function
C.__proto__ === Function.prototype
D.constructor === Function
// D is a function "indirectly" through the chain.
D.__proto__ === C
D.__proto__.__proto__ === Function.prototype
// "extends" sets up the prototype chain so that base class
// lookups will work as expected
var d = new D(1)
d.__proto__ === D.prototype
D.prototype.__proto__ === C.prototype
// This is what `d.inc` actually does.
d.__proto__.__proto__.inc === C.prototype.inc
// Class variables
// No ES6 syntax sugar apparently:
// /programming/22528967/es6-class-variable-alternatives
C.c = 1
C.c === 1
// Because `D.__proto__ === C`.
D.c === 1
// Nothing makes this work.
d.c === undefined

Diagrama simplificado sin todos los objetos predefinidos:

      __proto__
(C)<---------------(D)         (d)
| |                |           |
| |                |           |
| |prototype       |prototype  |__proto__
| |                |           |
| |                |           |
| |                | +---------+
| |                | |
| |                | |
| |                v v
|__proto__        (D.prototype)
| |                |
| |                |
| |                |__proto__
| |                |
| |                |
| | +--------------+
| | |
| | |
| v v
| (C.prototype)--->(inc)
|
v
Function.prototype
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
fuente
1

Los mejores ejemplos que he visto están en JavaScript de Douglas Crockford : The Good Parts . Definitivamente vale la pena comprarlo para ayudarlo a obtener una visión equilibrada del idioma.

Douglas Crockford es responsable del formato JSON y trabaja en Yahoo como un gurú de JavaScript.

Chris S
fuente
7
¿responsable? eso suena casi como "culpable de" :)
Roland Bouman
@Roland Creo que JSON es un formato bastante agradable y no detallado para almacenar datos. Sin embargo, definitivamente no lo inventó, el formato estaba allí para los ajustes de configuración en Steam en 2002
Chris S
Chris S, yo también lo creo. Cada vez más, desearía que todos hubiéramos podido omitir XML como formato de intercambio y pasar al JSON de inmediato.
Roland Bouman
3
No hay mucho que inventar: JSON es un subconjunto de la sintaxis literal de objetos propios de JavaScript, que ha estado en el lenguaje desde 1997.
Tim Down
@Tiempo buen punto - No me di cuenta de que ha estado ahí desde el principio
Chris S
0

Hay un fragmento de herencia basada en un prototipo de JavaScript con implementaciones específicas de la versión de ECMAScript. Automáticamente elegirá cuál usar entre las implementaciones de ES6, ES5 y ES3 de acuerdo con el tiempo de ejecución actual.

fuweichin
fuente
0

Añadiendo un ejemplo de herencia basada en prototipos en Javascript.

// Animal Class
function Animal (name, energy) {
  this.name = name;
  this.energy = energy;
}

Animal.prototype.eat = function (amount) {
  console.log(this.name, "eating. Energy level: ", this.energy);
  this.energy += amount;
  console.log(this.name, "completed eating. Energy level: ", this.energy);
}

Animal.prototype.sleep = function (length) {
  console.log(this.name, "sleeping. Energy level: ", this.energy);
  this.energy -= 1;
  console.log(this.name, "completed sleeping. Energy level: ", this.energy);
}

Animal.prototype.play = function (length) {
  console.log(this.name, " playing. Energy level: ", this.energy);
  this.energy -= length;
  console.log(this.name, "completed playing. Energy level: ", this.energy);
}

// Dog Class
function Dog (name, energy, breed) {
  Animal.call(this, name, energy);
  this.breed = breed;
}

Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

Dog.prototype.bark = function () {
  console.log(this.name, "barking. Energy level: ", this.energy);
  this.energy -= 1;
  console.log(this.name, "done barking. Energy level: ", this.energy);
}

Dog.prototype.showBreed = function () {
  console.log(this.name,"'s breed is ", this.breed);
}

// Cat Class
function Cat (name, energy, male) {
  Animal.call(this, name, energy);
  this.male = male;
}

Cat.prototype = Object.create(Animal.prototype);
Cat.prototype.constructor = Cat;

Cat.prototype.meow = function () {
  console.log(this.name, "meowing. Energy level: ", this.energy);
  this.energy -= 1;
  console.log(this.name, "done meowing. Energy level: ", this.energy);
}

Cat.prototype.showGender = function () {
  if (this.male) {
    console.log(this.name, "is male.");
  } else {
    console.log(this.name, "is female.");
  }
}

// Instances
const charlie = new Dog("Charlie", 10, "Labrador");
charlie.bark();
charlie.showBreed();

const penny = new Cat("Penny", 8, false);
penny.meow();
penny.showGender();

ES6 usa una implementación de herencia mucho más fácil con el uso de constructor y super palabras clave.


fuente