Implementazione del meccanismo di ereditarietà ECMAScript

Implementazione del meccanismo di ereditarietà

Per implementare il meccanismo di ereditarietà in ECMAScript, puoi iniziare dalla classe base da ereditare. Tutte le classi definite dai sviluppatori possono essere classi base. Per motivi di sicurezza, le classi locali e le classi host non possono essere classi base, in modo da prevenire l'accesso pubblico al codice compilato a livello di browser, che può essere utilizzato per attacchi maliziosi.

Dopo aver selezionato la classe base, puoi creare le sue sottoclassi. L'uso della classe base è completamente a tua scelta. A volte, potresti voler creare una classe base che non può essere utilizzata direttamente, che serve solo a fornire funzioni comuni alle sottoclassi. In questo caso, la classe base viene considerata astratta.

Nonostante ECMAScript non definisca rigorosamente le classi astratte come altri linguaggi, a volte crea classi che non sono ammesse all'uso. Di solito, chiamiamo queste classi classi astratte.

Le sottoclassi create erediteranno tutte le proprietà e i metodi dell'astratto, inclusi i costruttori e l'esecuzione dei metodi. Ricorda, tutte le proprietà e i metodi sono pubblici, quindi le sottoclassi possono accedere direttamente a questi metodi. Le sottoclassi possono anche aggiungere nuove proprietà e metodi che non esistono nell'astratto, o sovrascrivere le proprietà e i metodi dell'astratto.

Modi di ereditarietà

Come altre funzionalità, ci sono più modi per implementare l'ereditarietà in ECMAScript. Questo è perché il meccanismo di ereditarietà in JavaScript non è specificato chiaramente, ma realizzato tramite imitazione. Questo significa che i dettagli di tutte le ereditarietà non vengono completamente gestiti dal programma di interpretazione. Come sviluppatore, hai il diritto di decidere il modo di ereditarietà più adatto.

Di seguito ti presentiamo alcuni modi specifici di ereditarietà.

Imitazione di oggetti

Quando si ha in mente di progettare ECMAScript, non c'era l'intenzione di progettare l'imitazione di oggetti (object masquerading). È emerso solo dopo che gli sviluppatori hanno iniziato a comprendere come funzionano le funzioni, in particolare come utilizzare il prefisso this nelle funzioni ambientali.

Il principio è il seguente: il costruttore utilizza il prefisso this per assegnare tutti i valori alle proprietà e ai metodi (cioè utilizzare il costruttore dichiarato dalla classe). Poiché il costruttore è solo una funzione, può rendere il costruttore ClassA un metodo di ClassB e chiamarlo. ClassB riceverà le proprietà e i metodi definiti nel costruttore ClassA. Ad esempio, definire ClassA e ClassB nel modo seguente:

function ClassA(sColor) {
    this.color = sColor;
    this.sayColor = function () {
        alert(this.color);
    };
}
function ClassB(sColor) {
}

Ricordi? La parola chiave this si riferisce all'oggetto creato dal costruttore corrente. Tuttavia, in questo metodo, this si riferisce all'oggetto di proprietà. Questo principio è costruire un meccanismo di ereditarietà utilizzando ClassA come funzione regolare, piuttosto che come costruttore. Ecco come si può implementare il meccanismo di ereditarietà utilizzando il costruttore ClassB:

function ClassB(sColor) {
    this.newMethod = ClassA;
    this.newMethod(sColor);
    delete this.newMethod;
}

In questo codice, è stato assegnato un metodo newMethod a ClassA (ricorda, il nome della funzione è solo un puntatore a essa). Poi è stato chiamato questo metodo, passandogli come parametro il costruttore di ClassB sColor. L'ultima riga di codice ha eliminato il riferimento a ClassA, quindi non può essere chiamato in futuro.

Tutte le nuove proprietà e i nuovi metodi devono essere definiti dopo aver rimosso la riga di codice del nuovo metodo. Altrimenti, potrebbe sovrascrivere le proprietà e i metodi del superclasse correlate:

function ClassB(sColor, sName) {
    this.newMethod = ClassA;
    this.newMethod(sColor);
    delete this.newMethod;
    this.name = sName;
    this.sayName = function () {
        alert(this.name);
    };
}

Per dimostrare l'efficacia del codice precedente, è possibile eseguire l'esempio seguente:

var objA = new ClassA("blue");
var objB = new ClassB("red", "John");
objA.sayColor();	//Output "blue"
objB.sayColor();	//Output "red"
objB.sayName(); // Output: "John"

L'imitazione di oggetti può implementare l'ereditarietà multipla

Curiosamente, l'imitazione di oggetti può supportare l'ereditarietà multipla. Questo significa che una classe può ereditare più superclassi. Il meccanismo di ereditarietà multipla rappresentato con UML è illustrato nella figura sottostante:

Esempio di diagramma UML di meccanismo di ereditarietà

Ad esempio, se ci sono due classi ClassX e ClassY, ClassZ vuole ereditare queste due classi, può utilizzare il seguente codice:

function ClassZ() {
    this.newMethod = ClassX;
    this.newMethod();
    delete this.newMethod;
    this.newMethod = ClassY;
    this.newMethod();
    delete this.newMethod;
}

TIY

Esiste un difetto: se ci sono due classi ClassX e ClassY che hanno attributi o metodi con lo stesso nome, ClassY ha la priorità più alta perché eredita dall'ultima classe. A parte questo piccolo problema, è facile implementare il meccanismo di ereditarietà multilevello utilizzando lo spoofing dell'oggetto.

A causa della popolarità di questo metodo di ereditarietà, la terza versione di ECMAScript ha aggiunto due metodi all'oggetto Function, ovvero call() e apply().

metodo call()

Il metodo call() è molto simile al metodo di spoofing dell'oggetto classico. Il primo parametro viene utilizzato come oggetto this. Gli altri parametri vengono passati direttamente al metodo stesso. Ad esempio:

function sayColor(sPrefix,sSuffix) {
    alert(sPrefix + this.color + sSuffix);
};
var obj = new Object();
obj.color = "blue";
sayColor.call(obj, "The color is ", "a very nice color indeed.");

In questo esempio, la funzione sayColor() è definita fuori dall'oggetto, anche se non appartiene a nessun oggetto, può ancora utilizzare la parola chiave this. L'attributo color dell'oggetto obj è uguale a blue. Quando si chiama il metodo call(), il primo parametro è obj, il che significa che il valore della parola chiave this nel metodo sayColor() deve essere assegnato a obj. I secondi e terzi parametri sono stringhe. Corrispondono ai parametri sPrefix e sSuffix del metodo sayColor(). Il messaggio generato "The color is blue, a very nice color indeed." viene visualizzato infine.

Per utilizzare questo metodo con i metodi di spoofing dell'oggetto dell'ereditarietà, è sufficiente sostituire il codice di assegnazione, chiamata e rimozione delle prime tre righe:

function ClassB(sColor, sName) {
    //this.newMethod = ClassA;
    //this.newMethod(color);
    //delete this.newMethod;
    ClassA.call(this, sColor);
    this.name = sName;
    this.sayName = function () {
        alert(this.name);
    };
}

TIY

In questo caso, dobbiamo far sì che la parola chiave this nella classe ClassA sia uguale all'oggetto ClassB creato di recente, quindi this è il primo parametro. Il secondo parametro sColor è un parametro unico per entrambe le classi.

metodo apply()

apply() metodo ha due parametri, usati come oggetto this e come array di parametri da passare alla funzione. Ad esempio:

function sayColor(sPrefix,sSuffix) {
    alert(sPrefix + this.color + sSuffix);
};
var obj = new Object();
obj.color = "blue";
sayColor.apply(obj, new Array("The color is ", "a very nice color indeed."));

Questo esempio è lo stesso dell'esempio precedente, ma ora viene chiamato il metodo apply(). Quando si chiama il metodo apply(), il primo parametro è sempre obj, il che significa che il valore del keyword this nel metodo sayColor() dovrebbe essere obj. Il secondo parametro è un array di due stringhe che corrispondono ai parametri sPrefix e sSuffix nel metodo sayColor(), e il messaggio generato è ancora "The color is blue, a very nice color indeed.", che verrà visualizzato.

Questo metodo viene anche utilizzato per sostituire i codici di assegnazione, chiamata e eliminazione del nuovo metodo nelle prime tre righe:

function ClassB(sColor, sName) {
    //this.newMethod = ClassA;
    //this.newMethod(color);
    //delete this.newMethod;
    ClassA.apply(this, new Array(sColor));
    this.name = sName;
    this.sayName = function () {
        alert(this.name);
    };
}

Allo stesso modo, il primo parametro è sempre this, il secondo parametro è un array con un solo valore color. È possibile passare l'intero oggetto arguments di ClassB come secondo parametro al metodo apply():

function ClassB(sColor, sName) {
    //this.newMethod = ClassA;
    //this.newMethod(color);
    //delete this.newMethod;
    ClassA.apply(this, arguments);
    this.name = sName;
    this.sayName = function () {
        alert(this.name);
    };
}

TIY

Naturalmente, è possibile passare l'oggetto di parametri solo quando l'ordine dei parametri nella superclasse coincide esattamente con l'ordine dei parametri nella sottoclasse. Se non è così, è necessario creare un array separato e posizionare i parametri nel corretto ordine. Inoltre, è possibile utilizzare il metodo call().

Catena di prototype (prototype chaining)

Questo tipo di ereditarietà è stato originariamente utilizzato per la catena di prototype in ECMAScript. Il capitolo precedente ha introdotto il modo di definire una classe tramite prototype. La catena di prototype ha esteso questo modo, implementando in modo interessante il meccanismo di ereditarietà.

Come abbiamo appreso nel capitolo precedente, l'oggetto prototype è un modello, su cui si basano tutti gli oggetti da istanziare. In sintesi, qualsiasi attributo o metodo dell'oggetto prototype viene passato a tutti gli istanti della classe. La catena di prototype utilizza questa funzionalità per implementare il meccanismo di ereditarietà.

Se si ridefinisce in modo prototipale la classe di esempio precedente, essa diventa della seguente forma:

function ClassA() {
}
ClassA.prototype.color = "blue";
ClassA.prototype.sayColor = function () {
    alert(this.color);
};
function ClassB() {
}
ClassB.prototype = new ClassA();

La meravigliosa caratteristica del metodo prototype risiede nella riga di codice evidenziata in blu. Qui, l'attributo prototype di ClassB viene impostato su un'istanza di ClassA. Questo è molto interessante perché si desidera ottenere tutte le proprietà e i metodi di ClassA, ma non si desidera aggiungerli uno per uno all'attributo prototype di ClassB. C'è un modo migliore di assegnare un'istanza di ClassA all'attributo prototype?

Attenzione:Chiamare il costruttore di ClassA senza passargli parametri. Questo è un comportamento standard nella catena degli oggetti. Assicurati che il costruttore non abbia alcun parametro.

Simile all'imitazione dell'oggetto, tutte le proprietà e i metodi del sotto-oggetto devono apparire dopo l'attributo prototype è assegnato, perché prima di ciò tutti i metodi assegnati vengono eliminati. Perché? Perché l'attributo prototype viene sostituito con un nuovo oggetto, e l'oggetto originale che ha aggiunto i metodi viene distrutto. Quindi, il codice per aggiungere l'attributo name e il metodo sayName() alla classe ClassB è il seguente:

function ClassB() {
}
ClassB.prototype = new ClassA();
ClassB.prototype.name = "";
ClassB.prototype.sayName = function () {
    alert(this.name);
};

Puoi testare questo codice eseguendo l'esempio seguente:

var objA = new ClassA();
var objB = new ClassB();
objA.color = "blue";
objB.color = "red";
objB.name = "John";
objA.sayColor();
objB.sayColor();
objB.sayName();

TIY

Inoltre, nel contesto della catena degli oggetti, il comportamento dell'operatore instanceof è anche molto unico. Per tutti gli istanti di ClassB, instanceof restituisce true sia per ClassA che per ClassB. Ad esempio:

var objB = new ClassB();
alert(objB instanceof ClassA);	// Output: "true"
alert(objB instanceof ClassB);	// Output: "true"

Nel mondo debole del tipo ECMAScript, questo è uno strumento estremamente utile, ma non può essere utilizzato durante l'imitazione dell'oggetto.

Uno dei difetti della catena degli oggetti è che non supporta l'ereditarietà multipla. Ricorda, la catena degli oggetti sovrascriverà l'attributo prototype della classe con un altro tipo di oggetto.

Modo misto

Questo metodo di ereditarietà utilizza il costruttore per definire la classe, non utilizza nessun原型。Il problema principale dell'imitazione dell'oggetto è che deve utilizzare il metodo del costruttore, che non è la scelta migliore. Tuttavia, se si utilizza la catena degli oggetti, non si può utilizzare il costruttore con parametri. Come può scegliere lo sviluppatore? La risposta è semplice, entrambi.

Nell'ultimo capitolo, abbiamo spiegato il modo migliore per creare una classe, ovvero definire le proprietà con i costruttori e i metodi con i prototipi. Questo modo si applica anche al meccanismo di ereditarietà, dove l'oggetto si finge eredita le proprietà del costruttore e i metodi del prototipo attraverso la catena prototipale. Riportiamo l'esempio precedente riscritto con questi due modi, come segue:

function ClassA(sColor) {
    this.color = sColor;
}
ClassA.prototype.sayColor = function () {
    alert(this.color);
};
function ClassB(sColor, sName) {
    ClassA.call(this, sColor);
    this.name = sName;
}
ClassB.prototype = new ClassA();
ClassB.prototype.sayName = function () {
    alert(this.name);
};

In questo esempio, il meccanismo di ereditarietà è implementato dalle due righe in evidenza blu. Nella prima riga in evidenza, nel costruttore ClassB, l'oggetto si finge eredita l'attributo sColor della classe ClassA. Nella seconda riga in evidenza, i metodi della classe ClassA vengono ereditati tramite la catena prototipale. Poiché questo modo misto utilizza la catena prototipale, l'operatore instanceof può ancora funzionare correttamente.

Esempio seguente testato questo codice:

var objA = new ClassA("blue");
var objB = new ClassB("red", "John");
objA.sayColor();	//Output "blue"
objB.sayColor();	//Output "red"
objB.sayName();	//Output "John"

TIY