2010-09-01 11 views
20

Penso di aver frainteso il funzionamento dell'ereditarietà del prototipo Javascript. Nello specifico, le variabili interne dei prototipi sembrano essere condivise tra più sotto-oggetti diversi. È facile illustrare con codice:Variabili private nei prototipi ereditati

var A = function() 
{ 
    var internal = 0; 
    this.increment = function() 
    { 
    return ++internal; 
    }; 
}; 

var B = function() {}; 
// inherit from A 
B.prototype = new A; 

x = new B; 
y = new B; 

$('#hello').text(x.increment() + " - " + y.increment());​ 

Questo fornisce in uscita 1 - 2 (test su JSBin), mentre aspettavo il risultato sia 1 - 1, dato che volevo due oggetti separati.

Come posso essere sicuro che l'oggetto A non sia un oggetto condiviso tra più istanze di B?

Aggiornamento: This article mette in evidenza alcuni dei problemi:

Il problema è che il campo di applicazione ogni approccio utilizza per creare una variabile privata, che funziona bene, è anche la chiusura, in azione, che i risultati in se si modifica una variabile privata per un'istanza di oggetto, viene modificata per tutti. Cioè è più simile a una proprietà statica privata, che a una variabile privata effettiva.

Quindi, se si desidera avere qualcosa di privato, più come una costante non pubblica, uno qualsiasi degli approcci precedenti è buono, ma non per le vere variabili private. Le variabili private funzionano davvero bene solo con gli oggetti Singleton in JavaScript.

Soluzione: Come per la risposta di BGerrissen, cambiando la dichiarazione di B e lasciando del prototipo funziona come previsto:

var B = function() { A.apply(this, arguments); }; 

risposta

17

I membri privati ​​sono difficili utilizzando l'ereditarietà prototipica. Per uno, non possono essere ereditati. Devi creare membri privati ​​in ogni singolo costruttore. Puoi farlo applicando il super costruttore nella sottoclasse o creando un decoratore.

Decorator esempio:

function internalDecorator(obj){ 
    var internal = 0; 
    obj.increment = function(){ 
     return ++internal; 
    } 
} 

var A = function(){ 
    internalDecorator(this); 
} 
A.prototype = {public:function(){/*etc*/}} 

var B = function(){ 
    internalDecorator(this); 
} 
B.prototype = new A(); // inherits 'public' but ALSO redundant private member code. 

var a = new B(); // has it's own private members 
var b = new B(); // has it's own private members 

Questa è solo una variante della chiamata super-costruttore, è anche possibile ottenere lo stesso chiamando il costruttore vero e proprio super-con .apply()

var B = function(){ 
    A.apply(this, arguments); 
} 

Ora applicando eredità tramite B.prototype = new A() si richiama il codice costruttore non necessario da A.Un modo per evitare questo è quello di utilizzare Douglas Crockfords metodo Beget:

Object.beget = function(obj){ 
    var fn = function(){} 
    fn.prototype = obj; 
    return new fn(); // now only its prototype is cloned. 
} 

che si utilizza come segue:

B.prototype = Object.beget(A.prototype); 

Naturalmente, è possibile abbandonare l'eredità del tutto e fare buon uso di decoratori, almeno dove sono necessari membri privati.

5

Il punto del prototipo è che essa è condivisa tra più oggetti (cioè quelli che sono creati dalla stessa funzione di costruzione). Se sono necessarie variabili non condivise tra oggetti che condividono un prototipo, è necessario mantenere tali variabili all'interno della funzione di costruzione per ciascun oggetto. Basta usare il prototipo per condividere i metodi.

Nel tuo esempio, non puoi fare esattamente quello che vuoi usando i prototipi. Ecco cosa potresti fare invece. Vedere la risposta di Daniel Earwicker per ulteriori spiegazioni sul fatto che ora non ho più senso replicare qui.

var A = function() {}; 

A.prototype.incrementPublic = function() 
{ 
    return ++this.publicProperty; 
}; 

var B = function() 
{ 
    this.publicProperty = 0; 
    var internal = 0; 
    this.incrementInternal = function() 
    { 
     return ++internal; 
    }; 
}; 

B.prototype = new A(); 

var x = new B(), y = new B(); 
console.log(x.incrementPublic(), y.incrementPublic()); // 1, 1 
console.log(x.incrementInternal(), y.incrementInternal()); // 1, 1 
+1

Ma il punto qui è che la funzionalità appartiene a 'A', ed è condivisa tra più sub "classi"(' B', 'C' e 'D'). Non c'è possibilità per variabili private non statiche nelle superclassi in Javascript? –

+1

Un modo comune per indicare le intenzioni "private" (ovvero i programmatori di avvisi che qualcosa di speciale sta accadendo con una proprietà) consiste nell'usare il carattere di sottolineatura come prefisso al nome della proprietà (ad esempio obj._privateMember). Sebbene questo funzioni con l'ereditarietà di stringhe e numeri, inizia a diventare più complesso quando si utilizzano Oggetti e Matrici come proprietà. – BGerrissen

+0

Non sono chiaro esattamente cosa stai chiedendo. Vuoi che una variabile definita all'interno della funzione 'A' sia disponibile per i metodi di' B', 'C' e' D', ma da nessun'altra parte? Cerca di non pensare a cose in JavaScript in termini di funzionalità di altri linguaggi (come Java o C#). JavaScript semplicemente non ha classi. Ha oggetti che ereditano le proprietà da altri oggetti tramite una catena di prototipi. –

15

Hai bisogno di dimenticare l'idea di classi. Non c'è una cosa del genere in JavaScript come "istanza di B". C'è solo 'qualche oggetto che hai ottenuto chiamando la funzione di costruzione B'. Un oggetto ha proprietà. Alcune sono proprietà "proprie", altre sono incluse nella ricerca della catena del prototipo.

Quando dici new A, stai creando un oggetto. Quindi lo assegni come prototipo per B, il che significa che ogni chiamata a new B produce un nuovo oggetto che ha lo stesso prototipo diretto e quindi la stessa variabile contatore.

Nella risposta di Tim Down, vengono mostrate due alternative. Il suo incrementPublic utilizza l'ereditarietà, ma rende pubblica la variabile contatore (cioè, fornisce l'incapsulamento). Mentre lo incrementInternal rende il contatore privato ma riesce spostando il codice in B (cioè si rinuncia all'ereditarietà).

quello che vuoi è una combinazione di tre cose:

  • comportamento ereditabile - quindi deve essere definito in A e non richiedono il codice in B a parte l'impostazione del prototipo
  • dati privati, memorizzati in chiusura - variabili locali
  • dati per istanza, memorizzati in this.

Il problema è la contraddizione tra gli ultimi due. Vorrei anche dire che l'ereditarietà ha comunque un valore limitato in JS. E 'meglio di trattarlo come un linguaggio funzionale:

// higher-order function, returns another function with counter state 
var makeCounter = function() { 
    var c = 0; 
    return function() { return ++c; }; 
}; 

// make an object with an 'increment' method: 
var incrementable = { 
    increment: makeCounter() 
}; 

Personalmente tendo a evitare di funzioni di costruzione e prototipi eredità la maggior parte del tempo. Sono molto meno utili in JS di quanto suppongono le persone provenienti da un background OO.

Aggiornamento sarei diffidare della dichiarazione che hai citato nel vostro aggiornamento:

variabili private solo funzionano davvero bene con oggetti Singleton in JavaScript.

Questo non è proprio vero. Un oggetto è solo un "dizionario" di proprietà, ognuna delle quali può essere una funzione. Quindi dimentica gli oggetti e pensa alle funzioni. È possibile creare più istanze di una funzione secondo un modello, scrivendo una funzione che restituisce una funzione. L'esempio di makeCounter è solo un semplice esempio di ciò. makeCounter non è un "oggetto singleton" e non deve essere limitato all'utilizzo in oggetti singleton.

+1

+1. Buona spiegazione Lascerò la mia risposta così com'è e non cercherò di riscrivere ciò che hai scritto qui. Personalmente, trovo i prototipi e i costruttori utili, se non altro per la condivisione di metodi tra più oggetti correlati. –

+0

+1 per illuminare la natura prototipica. – BGerrissen

0

Ho appena trovato un'altra soluzione difficile, senza esportare metodi/variabili in oggetti pubblici.

function A(inherit) { 
    var privates = { //setup private vars, they could be also changed, added in method or children 
     a: 1, 
     b: 2, 
     c: 3 
    }; 
    //setup public methods which uses privates 
    this.aPlus = bindPlus("aPlus", this, privates); //pass method name as string! 
    this.aGet = bindPlus("aGet", this, privates); 
    if (inherit) { 
     return privates; 
    } 
} 
A.prototype.aPlus = function() { 
    var args = getArgs(arguments), 
     //self is "this" here 
     self = args.shift(), 
     privates = args.shift(), 
     //function real arguments 
     n = args.shift(); 
    return privates.a += n; 
}; 

A.prototype.aGet = function (n) { 
    var args = getArgs(arguments), 
     self = args.shift(), 
     privates = args.shift(); 
    console.log(this, self, privates); 
    return privates.a; 
}; 

//utilites 
function getArgs(arg) { 
    return Array.prototype.slice.call(arg); 
} 

function bindPlus(funct, self, privates) { 
    return function() { 
     return Object.getPrototypeOf(self)[funct].bind(this, self, privates).apply(null, arguments); 
    }; 
} 

//inherited 
function B(inherit) { 
    var privates = Object.getPrototypeOf(this).constructor.call(this, true); 
    privates.d = 4; 
    this.dGet = bindPlus("dGet", this, privates); 
    if (inherit) { 
     return privates; 
    } 
} 

B.prototype = Object.create(A.prototype); 
B.constructor = B; 

B.prototype.aGet = function() { 
    var args = getArgs(arguments), 
     self = args.shift(), 
     privates = args.shift(); 
    console.warn("B.aGet", this, privates); 
    return privates.a; 
}; 

B.prototype.dGet = function() { 
    var args = getArgs(arguments), 
     self = args.shift(), 
     privates = args.shift(); 
    console.warn("B.dGet", this, privates); 
    return privates.d; 
}; 


// tests 
var b = new B(); 
var a = new A(); 

//should be 223 
console.log("223 ?",b.aPlus(222)); 

//should be 42 
console.log("41",a.aPlus(222)); 

Più di prova e campioni qui: http://jsfiddle.net/oceog/TJH9Q/

Problemi correlati