MDN’s new design is in Beta! A sneak peek: https://blog.mozilla.org/opendesign/mdns-new-design-beta/

Ereditarietà e catena dei prototype

Questa traduzione è incompleta. Collabora alla traduzione di questo articolo dall’originale in lingua inglese.

JavaScript confonde un po' gli sviluppatori che hanno esperienza di linguaggi basati sulle classi (come Java o C++), siccome è un linguaggio dinamico e non fornisce un implementazione di class (la keyword class è introdotto in ES2015, ma è zucchero sintattico, Javascript rimarrà prototype-basato).

In termini di ereditarietà, Javascript ha solo un costrutto: gli oggetti. Ogni oggetto ha un link interno ad un altro oggetto chiamato prototype. Questo oggetto prototype ha a sua volta un suo prototype, e così via finché si raggiunge un oggetto con property nullnull, per definizione, non ha un prototype, ed agisce come link finale nella catena di prototipi.

Nonostante questo sia considerato spesso come una debolezza di Javascript, il modello di ereditarietà prototipale è invece più potente del modello classico. Per esempio, è banale costruire un classico modello sul modello prototipale, mentre il contrario è molto più difficile.

Ereditarietà con la catena di prototipi

Ereditare properties

Gli oggetti javaScript sono "contenitori" dinamici di properties (own properties). Gli ottetti JavaScript hanno un link ad un oggetto prototype. Provando ad accedere ad una property di un oggetto, la property non solo sarà ricercata solo sull'oggetto ma anche sul prototipo, sul prototipo del protitpo e così via fino a trovare una property con il nome specificat o fino alla fine della catena stessa.

Seguento lo standard ECMAScript, la notazione someObject.[[Prototype]] è usato per designare il prototype del someObject. Questo è equivalente alla property JavaScript __proto__ (ora deprecata). Poichè in ECMAScript 2015, [[Protoype]] è acceduta usando il metodi Object.getPrototypeOf()Object.setPrototypeOf().

Di seguito viene mostrato cosa succede quando si tenta l'accesso alla property:

// Assumiamo di avere un oggetto o, con le sue properties a and b:
// {a: 1, b: 2}
// o.[[Prototype]] ha le properties b and c:
// {b: 3, c: 4}
// Infine, o.[[Prototype]].[[Prototype]] é null.
// Questa è la fine della catena di prototipi poiché null, per defini// zione, non ha [[Prototype]].
// Così, l'intera catena di prototipi sarà:
// {a:1, b:2} ---> {b:3, c:4} ---> null

console.log(o.a); // 1
// C'è una property 'a' su o? Si, e il suo valore è 1.

console.log(o.b); // 2
// C'è una property 'b' su o? Si, e il suo valore è 2.
// Il prototype ha anche una property 'b', ma non è visitata. 
// Questa è chiamata "property shadowing"

console.log(o.c); // 4
// C'è una propria property 'c' su o? No, verifica il suo prototype.
// C'è una propria property 'c' su o.[[Prototype]]? si, il suo valore// è 4.

console.log(o.d); // undefined
// C'è una propria property 'd' su o? No, verifica il suo prototype.
// C'è una propria property 'd' su o.[[Prototype]]? No, verifica il suo prototype.
// o.[[Prototype]].[[Prototype]] è null, stop alla ricerca,
// nessuna property trovata, return undefined

Impostando una property su un oggetto viene creata una own property. La sola eccezione alle regole di comportamento setting e getting è quando c'è una property ereditata con un getter or a setter.

"Metodi" ereditati

JavaScript non ha "metodi" nella forma tipica dei linguaggi basati sulle classi. In JavaScript, qualunque funzione può essere aggiunta ad un oggetto come fosse property. Una funzione ereditata agisce come ogni altra property, incluse le  property shadowing come mostrato di seguito (in questo caso, una forma di sovrascrittura di metodi).

Quando viene eseguita una funzione ereditata, il valore del this punta all'oggetto ereditante, non all'oggetto prototype dove la funzione è una property proprietaria (own property).

var o = {
  a: 2,
  m: function(b){
    return this.a + 1;
  }
};

console.log(o.m()); // 3
// Chiamando o.m in questo caso, 'this' si riferisci a o

var p = Object.create(o);
// p è un oggeto che eredita da o

p.a = 12; // crea una propria property 'a' su p
console.log(p.m()); // 13
// quando p.m è chiamata, 'this' si riferisce a p.
// Così quando p eredita la funzione m di o, 
// 'this.a' significa p.a, the propria property 'a' di p

Differenti modi di creare oggetti e la risultatante catena di prototype

Oggetti creati con i costruttti sintattici

var o = {a: 1};

// L'oggetto o appena creato ha Object.prototype come proprio [[Prototype]]
// o non ha una propria property chamata 'hasOwnProperty'
// hasOwnProperty è una property propria di Object.prototype. 
// Quindi o eredita hasOwnProperty da Object.prototype
// Object.prototype ha null come suo prototype.
// o ---> Object.prototype ---> null

var a = ["yo", "whadup", "?"];

// Arrays ereditano da Array.prototype 
// (che metodi come indexOf, forEach, ecc.)
// La catena di prototype si presenta così:
// a ---> Array.prototype ---> Object.prototype ---> null

function f(){
  return 2;
}

// Le funzioni ereditano da Function.prototype 
// (che ha metodi come call, bind, ecc.)
// f ---> Function.prototype ---> Object.prototype ---> null

Con un constructor

Un "constructor" (costruttore) in JavaScript è semplicemente una funzione che è stata chiamata con  l' operatore new.

function Graph() {
  this.vertices = [];
  this.edges = [];
}

Graph.prototype = {
  addVertex: function(v){
    this.vertices.push(v);
  }
};

var g = new Graph();
// g è un oggetto con proprie properties 'vertices' e 'edges'.
// g.[[Prototype]] è il valore di Graph.prototype quando viene eseguito new Graph().

Con Object.create

ECMAScript 5 introduce un nuovo metodo: Object.create(). Chiamando questo metodo viene creato un nuovo oggetto. Il prototype di questo oggetto è il primo argomento della funzione:

var a = {a: 1}; 
// a ---> Object.prototype ---> null

var b = Object.create(a);
// b ---> a ---> Object.prototype ---> null
console.log(b.a); // 1 (ereditato)

var c = Object.create(b);
// c ---> b ---> a ---> Object.prototype ---> null

var d = Object.create(null);
// d ---> null
console.log(d.hasOwnProperty); 
// undefined, perché d non eredita da Object.prototype

Con la parola chiave class

ECMAScript 2015 introduce un nuovo gruppo di parole chiave per implementare le classi. Sebbene questi costrutti assomiglino a quelli familiari agli sviluppatori di linguaggi basati su classi, in realtà non cambia molto. JavaScript continua ad essere basato su prototype. Le nuove parole chiave includono class, constructor, static, extends, e super.

"use strict";

class Polygon {
  constructor(height, width) {
    this.height = height;
    this.width = width;
  }
}

class Square extends Polygon {
  constructor(sideLength) {
    super(sideLength, sideLength);
  }
  get area() {
    return this.height * this.width;
  }
  set sideLength(newLength) {
    this.height = newLength;
    this.width = newLength;
  }
}

var square = new Square(2);

Prestazioni

Il tempo impiegato per la ricerca che sono in alto nella catena dei prototype può avere un impatto negativo sulle prestazioni e questo può essere significativo in codice in cui le prestazioni sono critiche. Inoltre il tentativo di accedere a properties non esistenti esamina sempre la catena completa dei prototype.

In più, quando si itera sulle proprietà di un oggetto, tutte le properties enumerabili che si trovano nella sua catena dei prototype verranno enumerate.

Per controllare se un oggetto ha una property definata da se stesso e non da qualche parte nella catena di prototype, è necessario utilizzare il metodo hasOwnProperty che tutti gli oggetti ereditano da Object.prototype.

hasOwnProperty è la sola cosa in JavaScript che opera con le properties senza traversare la catena dei prototype.

Nota: non è sufficiente controllare se una property è undefined. La property può essere presente comunque e questo capita se il suo valore è stato assegnato undefined.

Cattiva pratica: Estensione di prototypes nativi

Una caratteristica mancante che viene spesso utilizzata è quella di estendere Object.prototype o uno degli altri prototype built-in.

Questa tecnica viene chiamata "monkey patching" e rompe l'incapsulazione. Nonostante sia utilizzata da frameworks popolari come ad esempio Prototype.js, non esistono comunque buone ragioni per appesantire i tipi built-in con funzionalità non-standard aggiuntive.

La sola buona ragione per estendere un prototype built-in è per dotare vecchie versioni di JavaScript con funzionalità presenti in quelle nuove; per esempio Array.forEach, etc.

Esempio

B erediterà da A:

function A(a){
  this.varA = a;
}

// Qual'è la ragione per includere varA nel prototype considerando che A.prototype.varA verrà sempre
// nascosto da this.varA, inserito nella definizione di gunzione soprastante?
A.prototype = {
  varA : null,  // Dovremmo togliere varA dal prototype visto che non fa niente?
      // forse serve come ottimizzazione per allocare spazio nelle classi nascoste?
      // https://developers.google.com/speed/articles/optimizing-javascript#Initializing instance variables
      // sarà valido se varA non viene inizializzato in modo unico per ogni istanza
  doSomething : function(){
    // ...
  }
}

function B(a, b){
  A.call(this, a);
  this.varB = b;
}
B.prototype = Object.create(A.prototype, {
  varB : {
    value: null, 
    enumerable: true, 
    configurable: true, 
    writable: true 
  },
  doSomething : { 
    value: function(){ // sovrascritto
      A.prototype.doSomething.apply(this, arguments); // chiama la classe superiore
      // ...
    },
    enumerable: true,
    configurable: true, 
    writable: true
  }
});
B.prototype.constructor = B;

var b = new B();
b.doSomething();

Le parti importanit sono:

  • I tipi sono definiti in .prototype
  • Si usa Object.create() per ereditare

prototype e Object.getPrototypeOf

JavaScript confonde un po' gli sviluppatori che provengono da Java o C++, essendo completamente dinamico, valutato tutto a runtime e non avendo classi in senso stretto. Sono tutte istanze di oggetti. Persino le "classi" che simuliamo sono semplicemente degli oggetti funzione.

Probabilmente hai già notato che la nostra funzione A ha una propery speciale chiamata prototype. Questa speciale property funziona con l'operatore new di JavaScript. Il riferimento all'oggetto prototype viene copiato nella property [[Prototype]] interna alla nuova istanza. Ad esempio, quando si fa var a1 = new A(), JavaScript (dopo aver creato l'oggetto in memoria e prima di eseguire la funzione A() con this definito ad esso) imposta a1. [[Prototype]] = A.prototype. Quando si accede poi alle properties dell'istanzaJavaScript prima controlla se esiste nell'oggetto direttamente e se non c'è guarda in [[Prototype]]. Questo significa che tutto ciò che viene definito in prototype viene effettivamente condiviso con tutte le istanze ed è possibile anche in seguito cambiare parti del prototype facendo comparire i cambiamenti in tutte le istanze esistenti, se è questo che si desidera.

Se, nell'esempio soprastante, si fa var a1 = new A(); var a2 = new A(); a1.doSomething farà riferimento a Object.getPrototypeOf(a1).doSomething, che è il medesimo di A.prototype.doSomething che è stato definito, perciò Object.getPrototypeOf(a1).doSomething == Object.getPrototypeOf(a2).doSomething == A.prototype.doSomething.

In breve, prototype è per i tipi, mentre Object.getPrototypeOf() da lo stesso risultato per le istanze.

[[Prototype]] viene esaminato ricursivamente, ad esempio a1.doSomething, Object.getPrototypeOf(a1).doSomething, Object.getPrototypeOf(Object.getPrototypeOf(a1)).doSomething ecc., finché viene trovato oppure Object.getPrototypeOf restituisce null.

Così, quando si chiama

var o = new Foo();

JavaScript in realtà esegue

var o = new Object();
o.[[Prototype]] = Foo.prototype;
Foo.call(o);

(o qualcosa di simile) e quando successivamente si esegue

o.someProp;

controlla se o ha pa property someProp. Se non c'è controlla Object.getPrototypeOf(o).someProp e se non c'è ancora controlla Object.getPrototypeOf(Object.getPrototypeOf(o)).someProp e via di seguito.

In conclusione

è essenziale capire il funzionamento dell'ereditarietà basata sul modello dei prototype prima di scrivere codice complesso che ne fa uso. Bisogna anche fare attenzione alla lunghezza della catena di prototype nel proprio codice ed accorciarla in caso di necessità per evitare possibili problemi di prestazioni. Infine, i prototype nativi non dovrebbero mai venire estesi per evitare problemi di compatibilità con nuove bunzionalità JavaScript che potrebbero essere introdotte.

Tag del documento e collaboratori

 Hanno collaborato alla realizzazione di questa pagina: kdex, claudiod, claudio.mantuano
 Ultima modifica di: kdex,