I prototipi sono un'ottimizzazione .
Un ottimo esempio di come usarli bene è la libreria jQuery. Ogni volta che si ottiene un oggetto jQuery utilizzando $('.someClass')
, quell'oggetto ha dozzine di "metodi". La libreria potrebbe ottenerlo restituendo un oggetto:
return {
show: function() { ... },
hide: function() { ... },
css: function() { ... },
animate: function() { ... },
// etc...
};
Ma ciò significherebbe che ogni oggetto jQuery in memoria avrebbe dozzine di slot con nome contenenti gli stessi metodi, più e più volte.
Invece, questi metodi sono definiti su un prototipo e tutti gli oggetti jQuery "ereditano" quel prototipo in modo da ottenere tutti quei metodi a un costo di runtime molto basso.
Una parte di vitale importanza di come jQuery lo fa bene è che questo è nascosto al programmatore. Viene considerata puramente un'ottimizzazione, non come qualcosa di cui devi preoccuparti quando usi la libreria.
Il problema con JavaScript è che le funzioni di costruzione semplici richiedono che il chiamante si ricordi di prefissarle new
o altrimenti in genere non funzionano. Non c'è una buona ragione per questo. jQuery fa bene nascondendo queste sciocchezze dietro una funzione ordinaria $
, quindi non devi preoccuparti di come vengono implementati gli oggetti.
Per poter creare comodamente un oggetto con un prototipo specificato, ECMAScript 5 include una funzione standard Object.create
. Una versione notevolmente semplificata di esso sarebbe simile a questa:
Object.create = function(prototype) {
var Type = function () {};
Type.prototype = prototype;
return new Type();
};
Si prende solo cura del dolore di scrivere una funzione di costruzione e poi chiamarla con new
.
Quando eviteresti i prototipi?
Un utile confronto è con i linguaggi OO più diffusi come Java e C #. Questi supportano due tipi di ereditarietà:
- ereditarietà dell'interfaccia , dove si trova
implement
un fileinterface
esempio che la classe fornisce la propria implementazione unica per ogni membro dell'interfaccia.
- implementazione eredità, in cui è
extend
una class
che fornisce le implementazioni di default di alcuni metodi.
In JavaScript, l'ereditarietà prototipica è una sorta di implementazione eredità . Quindi in quelle situazioni in cui (in C # o Java) saresti derivato da una classe base per ottenere un comportamento predefinito, a cui poi apporti piccole modifiche tramite override, quindi in JavaScript, l'ereditarietà prototipica ha senso.
Tuttavia, se ti trovi in una situazione in cui avresti utilizzato le interfacce in C # o Java, non hai bisogno di alcuna caratteristica del linguaggio particolare in JavaScript. Non è necessario dichiarare esplicitamente qualcosa che rappresenta l'interfaccia e non è necessario contrassegnare gli oggetti come "implementazione" di tale interfaccia:
var duck = {
quack: function() { ... }
};
duck.quack(); // we're satisfied it's a duck!
In altre parole, se ogni "tipo" di oggetto ha le proprie definizioni dei "metodi", non c'è valore nell'ereditare da un prototipo. Dopodiché, dipende da quante istanze allocare di ogni tipo. Ma in molti progetti modulari, c'è solo un'istanza di un dato tipo.
E infatti, è stato suggerito da molte persone che l'ereditarietà dell'implementazione è un male . Cioè, se ci sono alcune operazioni comuni per un tipo, allora forse è più chiaro se non vengono inserite in una classe base / super, ma sono invece esposte come funzioni ordinarie in qualche modulo, a cui si passa l'oggetto / gli oggetti vuoi che operino.