30 aprile 2022

Costruttore, operatore "new"

La sintassi {...} ci consente di creare un oggetto. Ma spesso abbiamo bisogno di creare multipli oggetti simili, come ad esempio più utenti, oggetti del menu e molto altro.

Questo può essere fatto utilizzando un costruttore e l’operatore "new".

Costruttore

Tecnicamente un costruttore è una normale funzione. Ma ci sono due convenzioni:

  1. Vengono denominati con la prima lettera maiuscola.
  2. Questi dovrebbero essere eseguiti solo con l’operatore "new".

Ad esempio:

function User(name) {
  this.name = name;
  this.isAdmin = false;
}

let user = new User("Jack");

alert(user.name); // Jack
alert(user.isAdmin); // false

Quando una funzione viene eseguita con new, esegue questi passaggi:

  1. Un nuovo oggetto, vuoto, viene creato ed assegnato a this.
  2. Viene eseguito il corpo della funzione. Solitamente questo modifica this, aggiungendo nuove proprietà.
  3. Viene ritornato il valore assegnato a this.

In altre parole, new User(...) fa qualcosa del genere:

function User(name) {
  // this = {};  (implicito)

  // aggiungiamo proprietà a this
  this.name = name;
  this.isAdmin = false;

  // return this;  (implicito)
}

Quindi let user = new User("Jack") dà lo stesso risultato di:

let user = {
  name: "Jack",
  isAdmin: false
};

Ora, se vogliamo creare altri utenti, possiamo chiamare new User("Ann"), new User("Alice") e cosi via. Molto più rapido rispetto all’utilizzare ogni volta oggetti letterali; risulta anche più facile da leggere.

Questo è il principale scopo dei costruttori – implementare codice riutilizzabile per la creazione di oggetti.

Ribadiamo – tecnicamente, ogni funzione (eccetto le arrow functions, siccome non hanno this) può essere utilizzata come costruttore. Ovvero: ogni funzione può essere eseguita con new. La “prima lettera maiuscola” è semplicemente una convenzione, per rendere esplicito che la funzione deve essere eseguita con new.

new function() { … }

Se abbiamo molte linee di codice utili alla creazione di un unico oggetto, possiamo raggrupparle in un costruttore richiamato contestualmente, come qui:

// create a function and immediately call it with new
let user = new function() {
  this.name = "John";
  this.isAdmin = false;

  // ...altro codice per la creazione di user
  // magari logiche complesse e istruzioni
  // variabili locali etc
};

Il costruttore non può essere chiamato nuovamente, perché non è salvato da nessuna parte; viene solo creato e chiamato. Questo trucco consente di incapsulare un codice che costruisce un singolo oggetto, senza necessità di riutilizzo futuro.

Costruttori modalità test: new.target

Tecniche avanzate

La sintassi presentata nella seguente sezione viene utilizzata raramente; potete tranquillamente saltarla se non vi interessa sapere proprio tutto.

Dentro la funzione, possiamo controllare quando questa viene chiamata con new e quando senza, utilizzando una speciale proprietà new.target.

Questa risulta ‘undefined’ per le chiamate normali, mentre contiene la funzione se viene chiamata con new:

function User() {
  alert(new.target);
}

// senza "new":
User(); // undefined

// con "new":
new User(); // function User { ... }

Questo può essere utilizzato per consentire ad entrambe le chiamate di funzionare (con new e senza), quindi sia in in “modalità costruttore” che in “modalità regolare”.

Possiamo anche fare in modo che le chiamate con new e quelle regolari facciano la stessa cosa, come in questo esempio:

function User(name) {
  if (!new.target) { // se mi esegui senza new
    return new User(name); // ...Aggiungo new al posto tuo
  }

  this.name = name;
}

let john = User("John"); // reindirizza la chiamata a new User
alert(john.name); // John

Questo approccio viene adottato in alcune librerie per rendere la sintassi più flessibile. Rende possibile la chiamata della funzione sia con new che senza.

Ma non è un’ottima cosa utilizzare la doppia sintassi ovunque, perché omettendo new il codice perde in leggibilità. Con la parola chiave new possiamo sapere con certezza che si sta creando un nuovo oggetto.

Return nel costruttore

Solitamente, i costruttori non hanno l’istruzione return. Il loro compito è di eseguire tutto ciò che è necessario a creare l’oggetto lavorando su this; quest’ultimo sarà il risultato.

Se decidiamo di inserire un’istruzione di return, vanno seguite delle semplici regole:

  • Se return viene invocato con un oggetto, questo verrà ritornato al posto di this.
  • Se return viene invocato con un tipo primitivo, verrà ignorato.

In altre parole, return con un oggetto ritorna quell’oggetto; in tutti gli altri casi verrà ritornato this.

Ad esempio, qui return sovrascrive this ritornando un oggetto: