Appendice

Eredità prototipale

Ad eccezione di null e undefined, ogni tipo di dati primitivi ha un prototipo, un wrapper dell'oggetto corrispondente che fornisce metodi per lavorare con i valori. Quando viene richiamato un metodo o una ricerca di proprietà su un elemento primitivo, JavaScript esegue il wrapping dell'elemento primitivo dietro le quinte e richiama il metodo o esegue la ricerca della proprietà sull'oggetto wrapper.

Ad esempio, un valore letterale di stringa non ha metodi propri, ma puoi chiamare il metodo .toUpperCase() grazie al corrispondente wrapper dell'oggetto String:

"this is a string literal".toUpperCase();
> THIS IS A STRING LITERAL

Si tratta dell'eredità prototipale: l'eredità di proprietà e metodi dal costruttore corrispondente di un valore.

Number.prototype
> Number { 0 }
>  constructor: function Number()
>  toExponential: function toExponential()
>  toFixed: function toFixed()
>  toLocaleString: function toLocaleString()
>  toPrecision: function toPrecision()
>  toString: function toString()
>  valueOf: function valueOf()
>  <prototype>: Object {  }

Puoi creare primitive utilizzando questi costruttori, anziché limitarti a definirli in base al loro valore. Ad esempio, l'utilizzo del costruttore String crea un oggetto stringa, non una stringa letterale: un oggetto che non contiene solo il valore della stringa, ma tutte le proprietà e i metodi ereditati del costruttore.

const myString = new String( "I'm a string." );

myString;
> String { "I'm a string." }

typeof myString;
> "object"

myString.valueOf();
> "I'm a string."

Per la maggior parte, gli oggetti risultanti si comportano come i valori che abbiamo utilizzato per definirli. Ad esempio, anche se la definizione di un valore numerico utilizzando il costruttore new Number genera un oggetto contenente tutti i metodi e le proprietà del prototipo Number, puoi utilizzare gli operatori matematici su questi oggetti come faresti con i valori numerici letterali:

const numberOne = new Number(1);
const numberTwo = new Number(2);

numberOne;
> Number { 1 }

typeof numberOne;
> "object"

numberTwo;
> Number { 2 }

typeof numberTwo;
> "object"

numberOne + numberTwo;
> 3

Dovrai utilizzare questi costruttori molto raramente, perché l'eredità prototipale integrata di JavaScript fa sì che non offrano alcun vantaggio pratico. Anche la creazione di primitive utilizzando i costruttori può portare a risultati imprevisti, perché il risultato è un oggetto, non un semplice letterale:

let stringLiteral = "String literal."

typeof stringLiteral;
> "string"

let stringObject = new String( "String object." );

stringObject
> "object"

Ciò può complicare l'utilizzo degli operatori di confronto rigoroso:

const myStringLiteral = "My string";
const myStringObject = new String( "My string" );

myStringLiteral === "My string";
> true

myStringObject === "My string";
> false

Inserimento automatico del punto e virgola (ASI)

Durante l'analisi di uno script, gli interpreti di JavaScript possono utilizzare una funzionalità chiamata inserimento automatico di punti e virgola (ASI) per provare a correggere le istanze di punti e virgola omessi. Se il parser JavaScript rileva un token non consentito, cerca di aggiungere un punto e virgola prima del token per correggere il potenziale errore di sintassi, a condizione che una o più delle seguenti condizioni siano vere:

  • Questo token è separato dal token precedente da un a capo.
  • Il token è }.
  • Il token precedente è ) e il punto e virgola inserito sarebbe il punto e virgola finale di un'istruzione dowhile.

Per ulteriori informazioni, consulta le regole ASI.

Ad esempio, l'omissione dei punti e virgola dopo le seguenti istruzioni non causerà un errore di sintassi a causa dell'ASI:

const myVariable = 2
myVariable + 3
> 5

Tuttavia, l'ASI non può tenere conto di più istruzioni nella stessa riga. Se scrivi più di un'istruzione nella stessa riga, assicurati di separarle con punti e virgola:

const myVariable = 2 myVariable + 3
> Uncaught SyntaxError: unexpected token: identifier

const myVariable = 2; myVariable + 3;
> 5

L'ASI è un tentativo di correzione degli errori, non un tipo di flessibilità sintattica integrata in JavaScript. Assicurati di utilizzare i punti e virgola dove opportuno, in modo da non fare affidamento su di essi per produrre codice corretto.

Modalità più restrittiva

Gli standard che regolano la scrittura di JavaScript si sono evoluti molto oltre quanto considerato durante la prima progettazione del linguaggio. Ogni nuova modifica al comportamento previsto di JavaScript deve evitare di causare errori nei siti web precedenti.

ES5 risolve alcuni problemi di vecchia data relativi alla semantica di JavaScript senza interrompere le implementazioni esistenti introducendo la "modalità rigorosa", un modo per attivare un insieme più restrittivo di regole del linguaggio per un intero script o per una singola funzione. Per attivare la modalità più restrittiva, utilizza la stringa letterale "use strict", seguita da un punto e virgola, nella prima riga di uno script o funzione:

"use strict";
function myFunction() {
  "use strict";
}

La modalità rigorosa impedisce determinate azioni "non sicure" o funzionalità obsolete, genera errori espliciti al posto di quelli comuni "silenziosi" e vieta l'uso di sintassi che potrebbero entrare in conflitto con le funzionalità future del linguaggio. Ad esempio, le prime decisioni di progettazione relative all'ambito delle variabili hanno aumentato la probabilità che gli sviluppatori "contaminassero" erroneamente l'ambito globale quando dichiaravano una variabile, indipendentemente dal contesto contenente, omettendo la parola chiave var:

(function() {
  mySloppyGlobal = true;
}());

mySloppyGlobal;
> true

I runtime JavaScript moderni non possono correggere questo comportamento senza il rischio di interrompere qualsiasi sito web che si basa su di esso, per errore o deliberatamente. Invece, il JavaScript moderno lo impedisce consentendo agli sviluppatori di attivare la modalità rigorosa per il nuovo lavoro e attivando la modalità rigorosa per impostazione predefinita solo nel contesto delle nuove funzionalità del linguaggio in cui non danneggeranno le implementazioni precedenti:

(function() {
    "use strict";
    mySloppyGlobal = true;
}());
> Uncaught ReferenceError: assignment to undeclared variable mySloppyGlobal

Devi scrivere "use strict" come letterale stringa. Un letterale del modello (use strict) non funzionerà. Devi anche includere "use strict" prima di qualsiasi codice eseguibile nel contesto previsto. In caso contrario, l'interprete lo ignora.

(function() {
    "use strict";
    let myVariable = "String.";
    console.log( myVariable );
    sloppyGlobal = true;
}());
> "String."
> Uncaught ReferenceError: assignment to undeclared variable sloppyGlobal

(function() {
    let myVariable = "String.";
    "use strict";
    console.log( myVariable );
    sloppyGlobal = true;
}());
> "String." // Because there was code prior to "use strict", this variable still pollutes the global scope

Per riferimento, per valore

Qualsiasi variabile, incluse le proprietà di un oggetto, i parametri di funzione e gli elementi di un array, set o map, può contenere un valore primitivo o un valore di riferimento.

Quando un valore primitivo viene assegnato da una variabile a un'altra, il motore JavaScript crea una copia del valore e la assegna alla variabile.

Quando assegni un oggetto (istanze di classe, array e funzioni) a una variabile, anziché creare una nuova copia dell'oggetto, la variabile contiene un riferimento alla posizione memorizzata dell'oggetto in memoria. Per questo motivo, la modifica di un oggetto a cui fa riferimento una variabile modifica l'oggetto a cui fa riferimento, non solo un valore contenuto nella variabile. Ad esempio, se inizializzi una nuova variabile con una variabile contenente un riferimento a un oggetto, poi utilizzi la nuova variabile per aggiungere una proprietà a quell'oggetto, la proprietà e il relativo valore vengono aggiunti all'oggetto originale:

const myObject = {};
const myObjectReference = myObject;

myObjectReference.myProperty = true;

myObject;
> Object { myProperty: true }

Questo è importante non solo per modificare gli oggetti, ma anche per eseguire confronti rigorosi, perché l'uguaglianza rigorosa tra oggetti richiede che entrambe le variabili facciano riferimento allo stesso oggetto per avere un valore true. Non possono fare riferimento a oggetti diversi, anche se sono strutturalmente identici:

const myObject = {};
const myReferencedObject = myObject;
const myNewObject = {};

myObject === myNewObject;
> false

myObject === myReferencedObject;
> true

Allocazione della memoria

JavaScript utilizza la gestione automatica della memoria, il che significa che la memoria non deve essere allocata o deallocata esplicitamente durante lo sviluppo. Anche se i dettagli degli approcci dei motori JavaScript alla gestione della memoria vanno oltre l'ambito di questo modulo, comprendere come viene allocata la memoria fornisce un utile contesto per lavorare con i valori di riferimento.

Esistono due "aree" in memoria: lo "stack" e l'"heap". Lo stack memorizza i dati statici, ovvero valori primitivi e riferimenti a oggetti, perché la quantità fissa di spazio necessaria per archiviare questi dati può essere allocata prima dell'esecuzione dello script. L'heap memorizza gli oggetti, che richiedono spazio allocato dinamicamente perché le loro dimensioni possono cambiare durante l'esecuzione. La memoria viene liberata da un processo chiamato "garbage collection", che rimuove dalla memoria gli oggetti senza riferimenti.

Il thread principale

JavaScript è un linguaggio fondamentalmente a thread singolo con un modello di esecuzione "sincrono", il che significa che può eseguire una sola attività alla volta. Questo contesto di esecuzione sequenziale è chiamato thread principale.

Il thread principale è condiviso da altre attività del browser, come l'analisi dell'HTML, il rendering e il rendering di parti della pagina, l'esecuzione di animazioni CSS e la gestione delle interazioni utente, dalle più semplici (come l'evidenziazione del testo) alle più complesse (come l'interazione con gli elementi dei moduli). I fornitori di browser hanno trovato modi per ottimizzare le attività eseguite dal thread principale, ma gli script più complessi possono comunque utilizzare troppe risorse del thread principale e influire sul rendimento complessivo della pagina.

Alcune attività possono essere eseguite in thread in background chiamati Web Worker, con alcune limitazioni:

  • I thread di lavoro possono agire solo su file JavaScript autonomi.
  • Hanno un accesso notevolmente ridotto o nullo alla finestra del browser e all'interfaccia utente.
  • Hanno limitazioni su come possono comunicare con il thread principale.

Queste limitazioni li rendono ideali per attività mirate e che richiedono molte risorse che altrimenti potrebbero occupare il thread principale.

Lo stack di chiamate

La struttura di dati utilizzata per gestire i "contesti di esecuzione", ovvero il codice in fase di esecuzione attiva, è un elenco chiamato stack di chiamate (spesso semplicemente "lo stack"). Quando uno script viene eseguito per la prima volta, l'interprete JavaScript crea un "contesto di esecuzione globale" e lo inserisce nello stack di chiamate, con le istruzioni all'interno di questo contesto globale eseguite una alla volta, dall'alto al basso. Quando l'interprete incontra una chiamata di funzione durante l'esecuzione del contesto globale, inserisce un "contesto di esecuzione della funzione" per quella chiamata nella cima della pila, mette in pausa il contesto di esecuzione globale ed esegue il contesto di esecuzione della funzione.

Ogni volta che viene chiamata una funzione, il contesto di esecuzione della funzione per quella chiamata viene inserito nella parte superiore dello stack, appena sopra il contesto di esecuzione corrente. Lo stack di chiamate funziona in base al criterio "ultimo arrivato, primo servito", il che significa che la chiamata di funzione più recente, che è la più alta nello stack, viene eseguita e continua fino alla risoluzione. Al termine della funzione, l'interprete la rimuove dallo stack delle chiamate e il contesto di esecuzione che contiene la chiamata di funzione ridiventa l'elemento più alto dello stack e riprende l'esecuzione.

Questi contesti di esecuzione acquisiscono tutti i valori necessari per la loro esecuzione. Inoltre, stabiliscono le variabili e le funzioni disponibili nell'ambito della funzione in base al contesto principale e determinano e impostano il valore della parola chiave this nel contesto della funzione.

La coda del loop di eventi e dei callback

Questa esecuzione sequenziale significa che le attività asincrone che includono funzioni di callback, come il recupero dei dati da un server, la risposta all'interazione dell'utente o l'attesa dei timer impostati con setTimeout o setInterval, bloccherebbero il thread principale fino al completamento dell'attività o interromperebbero inaspettatamente il contesto di esecuzione corrente nel momento in cui il contesto di esecuzione della funzione di callback viene aggiunto allo stack. Per risolvere il problema, JavaScript gestisce le attività asincrone utilizzando un "modello di concorrenza" basato su eventi composto dal "loop di eventi" e dalla "coda di callback" (a volte indicata come "coda di messaggi").

Quando un'attività asincrona viene eseguita nel thread principale, il contesto di esecuzione della funzione di callback viene inserito nella coda di callback, non sopra la pila di chiamate. Il loop di eventi è un pattern a volte chiamato reactor, che controlla continuamente lo stato della pila di chiamate e della coda di callback. Se nella coda di callback sono presenti attività e il loop di eventi determina che lo stack di chiamate è vuoto, le attività della coda di callback vengono inserite nello stack una alla volta per essere eseguite.