Javascript cuando usar prototipos

93

Me gustaría entender cuándo es apropiado usar métodos de prototipo en js. ¿Deben usarse siempre? ¿O hay casos en los que su uso no es preferido y / o incurre en una penalización de rendimiento?

Al buscar en este sitio métodos comunes para el espacio de nombres en js, parece que la mayoría usa una implementación no basada en prototipos: simplemente usa un objeto o un objeto de función para encapsular un espacio de nombres.

Viniendo de un lenguaje basado en clases, es difícil no intentar establecer paralelismos y pensar que los prototipos son como "clases" y las implementaciones del espacio de nombres que mencioné son como métodos estáticos.

opl
fuente

Respuestas:

133

Los prototipos son una optimización .

Un gran ejemplo de cómo usarlos bien es la biblioteca jQuery. Cada vez que obtiene un objeto jQuery utilizando $('.someClass'), ese objeto tiene docenas de "métodos". La biblioteca podría lograr eso devolviendo un objeto:

return {
   show: function() { ... },
   hide: function() { ... },
   css: function() { ... },
   animate: function() { ... },
   // etc...
};

Pero eso significaría que cada objeto jQuery en la memoria tendría docenas de ranuras con nombre que contienen los mismos métodos, una y otra vez.

En cambio, esos métodos se definen en un prototipo y todos los objetos jQuery "heredan" ese prototipo para obtener todos esos métodos a un costo de ejecución muy bajo.

Una parte de vital importancia de cómo jQuery lo hace bien es que esto está oculto al programador. Se trata simplemente como una optimización, no como algo de lo que tenga que preocuparse cuando use la biblioteca.

El problema con JavaScript es que las funciones de constructor desnudas requieren que la persona que llama recuerde ponerles un prefijo newo, de lo contrario, normalmente no funcionan. No hay una buena razón para ello. jQuery lo hace bien al ocultar esas tonterías detrás de una función ordinaria $, por lo que no tiene que preocuparse de cómo se implementan los objetos.

Para que pueda crear cómodamente un objeto con un prototipo específico, ECMAScript 5 incluye una función estándar Object.create. Una versión muy simplificada se vería así:

Object.create = function(prototype) {
    var Type = function () {};
    Type.prototype = prototype;
    return new Type();
};

Simplemente se encarga del dolor de escribir una función constructora y luego llamarla con new .

¿Cuándo evitarías los prototipos?

Una comparación útil es con lenguajes de OO populares como Java y C #. Estos admiten dos tipos de herencia:

  • interfaz de la herencia, en el que implementun interfacetal que la clase proporciona su propia implementación única para cada miembro de la interfaz.
  • aplicación de herencia, en el que extenduna classque proporciona implementaciones por defecto de algunos métodos.

En JavaScript, la herencia prototípica es una especie de herencia de implementación . Entonces, en aquellas situaciones en las que (en C # o Java) habría derivado de una clase base para obtener el comportamiento predeterminado, al que luego realiza pequeñas modificaciones a través de anulaciones, luego en JavaScript, la herencia prototípica tiene sentido.

Sin embargo, si se encuentra en una situación en la que habría utilizado interfaces en C # o Java, entonces no necesita ninguna característica de lenguaje en particular en JavaScript. No es necesario declarar explícitamente algo que represente la interfaz, y no es necesario marcar los objetos como "implementando" esa interfaz:

var duck = {
    quack: function() { ... }
};

duck.quack(); // we're satisfied it's a duck!

En otras palabras, si cada "tipo" de objeto tiene sus propias definiciones de los "métodos", entonces no tiene ningún valor heredar de un prototipo. Después de eso, depende de cuántas instancias asigne de cada tipo. Pero en muchos diseños modulares, solo hay una instancia de un tipo determinado.

Y de hecho, muchas personas han sugerido que la herencia de implementación es mala . Es decir, si hay algunas operaciones comunes para un tipo, entonces tal vez sea más claro si no se colocan en una clase base / super, sino que se exponen como funciones ordinarias en algún módulo, al que se le pasa el objeto (s) desea que operen.

Daniel Earwicker
fuente
1
Buena explicación. Entonces, ¿estaría de acuerdo en que, dado que considera que los prototipos son una optimización, siempre se pueden utilizar para mejorar su código? Me pregunto si hay casos en los que el uso de prototipos no tiene sentido o en realidad incurre en una penalización de rendimiento.
opl
En su seguimiento, menciona que "depende de cuántas instancias asigne de cada tipo". Pero el ejemplo al que se refiere no utiliza prototipos. ¿Dónde está la noción de asignar una instancia (todavía usaría "nuevo" aquí)? Además: digamos que el método quack tenía un parámetro: ¿cada invocación de duck.quack (param) haría que se creara un nuevo objeto en la memoria (tal vez sea irrelevante si tiene un parámetro o no)?
opl
3
1. Quise decir que si hubiera una gran cantidad de instancias de un tipo de pato, entonces tendría sentido modificar el ejemplo para que la quackfunción esté en un prototipo, al que están vinculadas las muchas instancias de pato. 2. La sintaxis literal del objeto { ... }crea una instancia (no es necesario utilizarla new). 3. Llamar a cualquier función JS hace que se cree al menos un objeto en la memoria - se llama el argumentsobjeto y almacena los argumentos pasados ​​en la llamada: developer.mozilla.org/en/JavaScript/Reference/…
Daniel Earwicker
Gracias acepté tu respuesta. Pero todavía tengo una ligera confusión con su punto (1): no entiendo lo que quiere decir con "gran número de casos de un tipo de pato". Como dijiste en (3), cada vez que llamas a una función JS, se crea un objeto en la memoria, por lo que incluso si solo tienes un tipo de pato, ¿no estarías asignando memoria cada vez que llamas a una función de pato (en en cuyo caso siempre tendría sentido utilizar un prototipo)?
opl
11
+1 La comparación con jQuery fue la primera explicación clara y concisa de cuándo y por qué usar prototipos que he leído. Muchas gracias.
GFoley83
46

Debería utilizar prototipos si desea declarar un método "no estático" del objeto.

var myObject = function () {

};

myObject.prototype.getA = function (){
  alert("A");
};

myObject.getB = function (){
  alert("B");
};

myObject.getB();  // This works fine

myObject.getA();  // Error!

var myPrototypeCopy = new myObject();
myPrototypeCopy.getA();  // This works, too.
KeatsKelleher
fuente
@keatsKelleher pero podemos crear un método no estático para el objeto simplemente definiendo el método dentro de la función constructora usando el thisejemplo this.getA = function(){alert("A")}¿verdad?
Amr Labib
17

Una razón para usar el prototypeobjeto integrado es si va a duplicar un objeto varias veces que compartirá una funcionalidad común. Al adjuntar métodos al prototipo, puede ahorrar en la creación de métodos duplicados para cada newinstancia. Pero cuando adjunta un método al prototype, todas las instancias tendrán acceso a esos métodos.

Digamos que tiene una Car()clase / objeto base .

function Car() {
    // do some car stuff
}

luego crea varias Car()instancias.

var volvo = new Car(),
    saab = new Car();

Ahora, sabe que cada automóvil deberá conducir, encender, etc. En lugar de adjuntar un método directamente a la Car()clase (que ocupa memoria por cada instancia creada), puede adjuntar los métodos al prototipo (creando los métodos solo once), dando acceso a esos métodos tanto al nuevo volvocomo a saab.

// just mapping for less typing
Car.fn = Car.prototype;

Car.fn.drive = function () {
    console.log("they see me rollin'");
};
Car.fn.honk = function () {
    console.log("HONK!!!");
}

volvo.honk();
// => HONK!!!
saab.drive();
// => they see me rollin'
hellatan
fuente
2
en realidad esto es incorrecto. volvo.honk () no funcionará porque reemplazó completamente el objeto prototipo, no lo extendió. Si tuviera que hacer algo como esto, funcionaría como espera: Car.prototype.honk = function () {console.log ('HONK');} volvo.honk (); // 'HONK'
29er
1
@ 29er - en la forma en que escribí este ejemplo, tienes razón. El orden sí importa. Si Car.prototype = { ... }tuviera que mantener este ejemplo como está, tendría que venir antes de llamar a new Car()como se ilustra en este jsfiddle: jsfiddle.net/mxacA . En cuanto a su argumento, esta sería la forma correcta de hacerlo: jsfiddle.net/Embnp . Lo curioso es que no recuerdo haber respondido esta pregunta =)
hellatan
@hellatan puede solucionarlo configurando constructor: Car en ya que sobrescribió la propiedad del prototipo con un objeto literal.
Josh Bedo
@josh gracias por señalar eso. Actualicé mi respuesta para no sobrescribir el prototipo con un objeto literal, como debería haber sido desde el principio.
hellatan
12

Coloque funciones en un objeto prototipo cuando vaya a crear muchas copias de un tipo particular de objeto y todas necesiten compartir comportamientos comunes. Al hacerlo, ahorrará algo de memoria al tener solo una copia de cada función, pero ese es solo el beneficio más simple.

Cambiar métodos en objetos prototipo, o agregar métodos, cambia instantáneamente la naturaleza de todas las instancias del tipo (s) correspondiente (s).

Ahora, exactamente por qué haría todas estas cosas es principalmente una función del diseño de su propia aplicación y el tipo de cosas que necesita hacer en el código del lado del cliente. (Una historia completamente diferente sería el código dentro de un servidor; mucho más fácil de imaginar haciendo más código "OO" a gran escala allí).

Puntiagudo
fuente
entonces, cuando creo una instancia de un nuevo objeto con métodos de prototipo (a través de una nueva palabra clave), ¿ese objeto no obtiene una nueva copia de cada función (solo una especie de puntero)? Si este es el caso, ¿por qué no querría utilizar un prototipo?
opl
como @marcel, d'oh ... =)
hellatan
@opi sí, tienes razón, no se hace ninguna copia. En cambio, los símbolos (nombres de propiedad) en el objeto prototipo están "allí" de forma natural como partes virtuales de cada objeto de instancia. La única razón por la que la gente no querría molestarse con eso serían los casos en que los objetos son de corta duración y distintos, o en los que no hay mucho "comportamiento" para compartir.
Puntiagudo
3

Si explico en el término basado en la clase, entonces Person es clase, walk () es el método Prototype. Así que walk () tendrá su existencia solo después de crear una instancia de un nuevo objeto con esto.

Entonces, si desea crear copias de objetos como Persona u, puede crear muchos usuarios. El prototipo es una buena solución, ya que ahorra memoria al compartir / heredar la misma copia de función para cada uno de los objetos en la memoria.

Mientras que la estática no es de gran ayuda en tal escenario.

function Person(){
this.name = "anonymous";
}

// its instance method and can access objects data data 
Person.prototype.walk = function(){
alert("person has started walking.");
}
// its like static method
Person.ProcessPerson = function(Person p){
alert("Persons name is = " + p.name);
}

var userOne = new Person();
var userTwo = new Person();

//Call instance methods
userOne.walk();

//Call static methods
Person.ProcessPerson(userTwo);

Entonces, con esto es más parecido al método de instancia. El enfoque del objeto es como los métodos estáticos.

https://developer.mozilla.org/en/Introduction_to_Object-Oriented_JavaScript

Anil Namde
fuente