Javascript quando utilizzare i prototipi


93

Mi piacerebbe capire quando è opportuno utilizzare metodi prototipo in js. Dovrebbero essere sempre usati? O ci sono casi in cui il loro utilizzo non è preferito e / o incorre in una penalità nelle prestazioni?

Nella ricerca in questo sito sui metodi comuni per lo spazio dei nomi in js, sembra che la maggior parte utilizzi un'implementazione non basata su prototipi: semplicemente utilizzando un oggetto o un oggetto funzione per incapsulare uno spazio dei nomi.

Provenendo da un linguaggio basato su classi, è difficile non cercare di tracciare paralleli e pensare che i prototipi siano come "classi" e le implementazioni dello spazio dei nomi che ho citato sono come metodi statici.

Risposte:


133

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 newo 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 implementun fileinterface esempio che la classe fornisce la propria implementazione unica per ogni membro dell'interfaccia.
  • implementazione eredità, in cui è extenduna classche 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.


1
Buona spiegazione. Allora sei d'accordo che, dal momento che consideri i prototipi come un'ottimizzazione, possono sempre essere usati per migliorare il tuo codice? Mi chiedo se ci sono casi in cui l'utilizzo di prototipi non ha senso o incorre effettivamente in una penalità nelle prestazioni.
opl

Nel tuo follow-up, dici che "dipende dal numero di istanze che assegni per ogni tipo". Ma l'esempio a cui fai riferimento non utilizza prototipi. Dov'è l'idea di allocare un'istanza (qui useresti ancora "new")? Inoltre: supponiamo che il metodo ciarlatano avesse un parametro: ogni invocazione di duck.quack (param) causerebbe la creazione di un nuovo oggetto in memoria (forse è irrilevante se ha un parametro o no)?
opl

3
1. Intendevo che se ci fosse un gran numero di istanze di un tipo di anatra, allora avrebbe senso modificare l'esempio in modo che la quackfunzione sia in un prototipo, a cui sono collegate le molte istanze di anatra. 2. La sintassi letterale dell'oggetto { ... }crea un'istanza (non è necessario utilizzarla newcon essa). 3. La chiamata a qualsiasi funzione JS causa la creazione di almeno un oggetto in memoria: si chiama argumentsoggetto e memorizza gli argomenti passati nella chiamata: developer.mozilla.org/en/JavaScript/Reference/…
Daniel Earwicker,

Grazie ho accettato la tua risposta. Ma ho ancora una leggera confusione con il tuo punto (1): non sto capendo cosa intendi per "numero elevato di casi di un tipo di anatra". Come hai detto in (3) ogni volta che chiami una funzione JS, un oggetto viene creato in memoria, quindi anche se hai solo un tipo di anatra, non alloceresti memoria ogni volta che chiami una funzione di anatra (in in questo caso avrebbe sempre senso utilizzare un prototipo)?
opl

11
+1 Il confronto con jQuery è stata la prima spiegazione chiara e concisa di quando e perché usare i prototipi che ho letto. Grazie mille.
GFoley83

46

Dovresti usare i prototipi se desideri dichiarare un metodo "non statico" dell'oggetto.

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 ma possiamo creare un metodo non statico per l'oggetto semplicemente definendo il metodo all'interno della funzione di costruzione usando l' thisesempio this.getA = function(){alert("A")}giusto?
Amr Labib

17

Un motivo per utilizzare l' prototypeoggetto integrato è se duplicherai più volte un oggetto che condividerà funzionalità comuni. Associando metodi al prototipo, è possibile risparmiare sui metodi di duplicazione creati per ogni newistanza. Ma quando colleghi un metodo al fileprototype , tutte le istanze avranno accesso a quei metodi.

Supponi di avere una Car()classe / oggetto di base .

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

quindi crei più Car()istanze.

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

Ora, sai che ogni macchina dovrà guidare, accendersi, ecc. Invece di allegare un metodo direttamente alla Car()classe (che occupa memoria per ogni istanza creata), puoi invece allegare i metodi al prototipo (creando solo i metodi una volta), dando quindi accesso a tali metodi sia al nuovo volvoche al file 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'

2
in realtà questo non è corretto. volvo.honk () non funzionerà perché hai completamente sostituito l'oggetto prototipo, non esteso. Se dovessi fare qualcosa di simile, funzionerebbe come ti aspetti: Car.prototype.honk = function () {console.log ('HONK');} volvo.honk (); // 'HONK'
29er

1
@ 29er - nel modo in cui ho scritto questo esempio, hai ragione. L'ordine conta. Se dovessi mantenere questo esempio così com'è, dovrei Car.prototype = { ... }venire prima di chiamare a new Car()come illustrato in questo jsfiddle: jsfiddle.net/mxacA . Per quanto riguarda il tuo argomento, questo sarebbe il modo corretto per farlo: jsfiddle.net/Embnp . La cosa divertente è che non ricordo di aver risposto a questa domanda =)
hellatan

@hellatan puoi risolverlo impostando il costruttore: Car su poiché hai sovrascritto la proprietà del prototipo con un oggetto letterale.
Josh Bedo

@ Josh grazie per averlo fatto notare. Ho aggiornato la mia risposta in modo da non sovrascrivere il prototipo con un oggetto letterale, come avrebbe dovuto essere dall'inizio.
hellatan

12

Metti le funzioni su un oggetto prototipo quando creerai molte copie di un particolare tipo di oggetto e tutte devono condividere comportamenti comuni. In questo modo, risparmierai un po 'di memoria avendo solo una copia di ogni funzione, ma questo è solo il vantaggio più semplice.

La modifica dei metodi sugli oggetti prototipo o l'aggiunta di metodi cambia istantaneamente la natura di tutte le istanze dei tipi corrispondenti.

Ora, esattamente il motivo per cui faresti tutte queste cose è principalmente una funzione della progettazione della tua applicazione e del tipo di cose che devi fare nel codice lato client. (Una storia completamente diversa sarebbe codice all'interno di un server; molto più facile immaginare di fare codice "OO" su larga scala lì.)


quindi quando istanzio un nuovo oggetto con metodi prototipo (tramite nuova parola chiave), quell'oggetto non ottiene una nuova copia di ogni funzione (solo una specie di puntatore)? Se questo è il caso, perché non vorresti utilizzare un prototipo?
opl

come @marcel, d'oh ... =)
hellatan

@opi sì, hai ragione - non è stata fatta alcuna copia. Invece, i simboli (nomi di proprietà) sull'oggetto prototipo sono semplicemente "lì" naturalmente come parti virtuali di ogni oggetto istanza. L'unico motivo per cui le persone non vorrebbero preoccuparsi di ciò sarebbero i casi in cui gli oggetti sono di breve durata e distinti, o dove non c'è molto "comportamento" da condividere.
punta

3

Se spiego in un termine basato sulla classe, Person è class, walk () è il metodo Prototype. Quindi walk () avrà la sua esistenza solo dopo aver istanziato un nuovo oggetto con questo.

Quindi, se vuoi creare le copie di oggetti come Person u puoi creare molti utenti Prototype è una buona soluzione in quanto consente di risparmiare memoria condividendo / ereditando la stessa copia di funzione per ciascuno degli oggetti in memoria.

Considerando che statico non è di grande aiuto in tale scenario.

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

Quindi con questo è più simile al metodo di istanza. L'approccio dell'oggetto è come i metodi statici.

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

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.