Javascript “OOP” e prototipi con ereditarietà a più livelli

Sono nuovo nella programmazione Javascript e mi sto avvicinando alla mia prima applicazione (un gioco, in effetti) da una prospettiva di programmazione orientata agli oggetti (so che js non è realmente orientato agli oggetti, ma per questo particolare problema è stato più facile iniziare come Questo).

Ho una gerarchia di “classi” in cui la class più alta (“Thing”) definisce una lista di cose correlate (elementi allegati nel gioco). È ereditato da una class ThingA che è ereditata dalle classi ThingA1 e ThingA2.

L’esempio minimo sarebbe:

function Thing() { this.relatedThings = []; } Thing.prototype.relateThing = function(what) { this.relatedThings.push(what); } ThingA.prototype = new Thing(); ThingA.prototype.constructor = ThingA; function ThingA() { } ThingA1.prototype = new ThingA(); ThingA1.prototype.constructor = ThingA1; function ThingA1() { } ThingA2.prototype = new ThingA(); ThingA2.prototype.constructor = ThingA2; function ThingA2() { } var thingList = []; thingList.push(new ThingA()); thingList.push(new ThingA1()); thingList.push(new ThingA2()); thingList.push(new ThingA2()); thingList.push(new Thing()); thingList[1].relateThing('hello'); 

Alla fine del codice, quando viene eseguito il relateThing, ogni ThingA, ThingA1 e ThingA2 lo eseguiranno (non l’ultimo object “Thing” nell’array). Ho trovato se definisco la funzione relateThing nel prototipo ThingA, funzionerà correttamente. A causa di come è progettato il gioco, preferirò non doverlo fare.

Forse non sto capendo qualcosa su come i prototipi funzionano in javascript. So che la funzione è condivisa tra tutti gli oggetti, ma suppongo che l’esecuzione sia individuale. Qualcuno potrebbe spiegare perché sta succedendo questo e come risolverlo? Non so se sto facendo l’ereditarietà sbagliata, o le definizioni dei prototipi, o cosa.

Grazie in anticipo.

Benvenuti nella catena del prototipo!

Vediamo come appare nel tuo esempio.

Il problema

Quando si chiama new Thing() , si crea un nuovo object con una proprietà relatedThings che fa riferimento a una matrice. Quindi possiamo dire di avere questo:

 +--------------+ |Thing instance| | | | relatedThings|----> Array +--------------+ 

Stai quindi assegnando questa istanza a ThingA.prototype :

 +--------------+ | ThingA | +--------------+ | | |Thing instance| | prototype |----> | | +--------------+ | relatedThings|----> Array +--------------+ 

Quindi ogni istanza di ThingA erediterà ThingA di Thing . Ora creerai ThingA1 e ThingA2 e assegnerai una nuova istanza ThingA a ciascuno dei loro prototipi, e successivamente creerai istanze di ThingA1 e ThingA2 (e ThingA e Thing , ma non mostrate qui).

La relazione ora è questa ( __proto__ è una proprietà interna , che collega un object con il suo prototipo):

  +-------------+ | ThingA | | | +-------------+ | prototype |----+ | ThingA1 | +-------------+ | | | | | prototype |---> +--------------+ | +-------------+ | ThingA | | | instance (1) | | | | | +-------------+ | __proto__ |--------------+ | ThingA1 | +--------------+ | | instance | ^ | | | | v | __proto__ |-----------+ +--------------+ +-------------+ |Thing instance| | | | relatedThings|---> Array +-------------+ +--------------+ +--------------+ | ThingA2 | | ThingA | ^ | | | instance (2) | | | prototype |---> | | | +-------------+ | __proto__ |--------------+ +--------------+ +-------------+ ^ | ThingA2 | | | instance | | | | | | __proto__ |-----------+ +-------------+ 

E a causa di ciò, ogni istanza di ThingA , ThingA1 o ThingA2 fa riferimento a una stessa istanza dell’array .

Questo non è quello che vuoi!


La soluzione

Per risolvere questo problema, ogni istanza di qualsiasi “sottoclass” dovrebbe avere la propria proprietà relatedThings . È ansible ottenere ciò chiamando il costruttore genitore in ogni costruttore figlio, in modo simile a chiamare super() in altre lingue:

 function ThingA() { Thing.call(this); } function ThingA1() { ThingA.call(this); } // ... 

Questo chiama Thing e ThingA e lo imposta all’interno di quella funzione sul primo argomento che si passa a .call . Ulteriori informazioni su .call [MDN] e this [MDN] .

Questo da solo cambierà l’immagine sopra a:

  +-------------+ | ThingA | | | +-------------+ | prototype |----+ | ThingA1 | +-------------+ | | | | | prototype |---> +--------------+ | +-------------+ | ThingA | | | instance (1) | | | | | | relatedThings|---> Array | +-------------+ | __proto__ |--------------+ | ThingA1 | +--------------+ | | instance | ^ | | | | | |relatedThings|---> Array | v | __proto__ |-----------+ +--------------+ +-------------+ |Thing instance| | | | relatedThings|---> Array +-------------+ +--------------+ +--------------+ | ThingA2 | | ThingA | ^ | | | instance (2) | | | prototype |---> | | | +-------------+ | relatedThings|---> Array | | __proto__ |--------------+ +--------------+ +-------------+ ^ | ThingA2 | | | instance | | | | | |relatedThings|---> Array | | __proto__ |-----------+ +-------------+ 

Come puoi vedere, ogni istanza ha la sua proprietà relatedThings , che si riferisce a una diversa istanza di array. Ci sono ancora proprietà relatedThings nella catena del prototipo, ma sono tutte ombreggiate dalla proprietà dell’istanza.


Migliore ereditarietà

Inoltre, non impostare il prototipo con:

 ThingA.prototype = new Thing(); 

In realtà non vuoi creare una nuova istanza di Thing qui. Cosa accadrebbe se Thing aspettasse argomenti? Quale passeresti? Cosa succede se chiamare il costruttore Thing ha degli effetti collaterali?

Quello che vuoi veramente è colbind Thing.prototype alla catena del prototipo. Puoi farlo con Object.create [MDN] :

 ThingA.prototype = Object.create(Thing.prototype); 

Tutto ciò che accade quando viene eseguito il costruttore ( Thing ) avverrà più tardi, quando creiamo effettivamente una nuova istanza ThingA (chiamando Thing.call(this) come mostrato sopra).

Se non ti piace il modo in cui la prototipazione funziona in JavaScript per ottenere un modo semplice di ereditarietà e OOP, ti suggerisco di dare un’occhiata a questo: https://github.com/haroldiedema/joii

In pratica ti permette di fare quanto segue (e altro):

 // First (bottom level) var Person = new Class(function() { this.name = "Unknown Person"; }); // Employee, extend on Person & apply the Role property. var Employee = new Class({ extends: Person }, function() { this.name = 'Unknown Employee'; this.role = 'Employee'; }); // 3rd level, extend on Employee. Modify existing properties. var Manager = new Class({ extends: Employee }, function() { // Overwrite the value of 'role'. this.role = 'Manager'; // Class constructor to apply the given 'name' value. this.__construct = function(name) { this.name = name; } }); // And to use the final result: var myManager = new Manager("John Smith"); console.log( myManager.name ); // John Smith console.log( myManager.role ); // Manager 

In OOP, quando stai definendo un costruttore di una sub-class sei anche (implicitamente o esplicitamente) scegliendo un costruttore dal super-tipo. Quando viene costruito un sub-object vengono eseguiti entrambi i costruttori, prima quello dalla super-class, l’altro l’altro verso il basso della gerarchia.

In javascript questo deve essere esplicitamente chiamato e non venire automaticamente!

 function Thing() { this.relatedThings = []; } Thing.prototype.relateThing = function(what){ this.relatedThings.push(what); } function ThingA(){ Thing.call(this); } ThingA.prototype = new Thing(); function ThingA1(){ ThingA.call(this); } ThingA1.prototype = new ThingA(); function ThingA2(){ ThingA.call(this); } ThingA2.prototype = new ThingA(); 

Se non lo fai, tutte le istanze di ThingA, ThingA1 e ThingA2 hanno lo stesso array relatedThings costruito nel costruttore Thing e chiamato una volta per tutte le istanze alla riga:

 ThingA.prototype = new Thing(); 

A livello globale, infatti, nel tuo codice, hai solo 2 chiamate al costruttore Thing e questo ha come risultato solo 2 istanze dell’array relatedThings.