Sommario

    Tipi di dato in JavaScript

    I tipi di dato sono classificazioni che definiscono il genere di valore che una variabile può contenere. Numeri interi e decimali, testo, valori logici, ad esempio, rappresentano categorie ricorrenti in qualunque linguaggio di programmazione. La scelta del tipo di dato in JavaScript ha implicazioni importanti su come il dato stesso viene salvato, copiato e confrontato.

    Tipizzazione debole

    JavaScript è un linguaggio debolmente e dinamicamente tipizzato, quindi le variabili non richiedono una dichiarazione di tipo esplicita e possono cambiare tipo in fase di runtime (ovvero durante l’esecuzione del codice).

    Conversione implicita

    Una conseguenza diretta della tipizzazione debole è la conversione implicita quando un’operazione coinvolge tipi differenti. In poche parole JavaScript, per far funzionare le operazioni con tipi discordanti effettua automaticamente delle operazioni di conversione invece di generare un errore.

    Le conversioni implicite conferiscono flessibilità al linguaggio, ma possono soprattutto creare bug difficili da scovare quando le conversioni avvengono dove non sono previste o dove ci si aspetterebbe che avvengano in modo differente.

    Tipi primitivi e oggetti

    Fino a questo punto abbiamo constatato che esistono dei tipi di dato che vengono gestiti dinamicamente dal linguaggio ma non è necessario dichiararli quando si inizializza una variabile. È importante sapere, inoltre, che i tipi di dato si dividono in due grandi macrocategorie:

    1. tipi primitivi o atomici;
    2. oggetti o tipi strutturati.

    I primi sono dei tipi di dato semplici e immutabili che possono contenere una sola entità: principalmente un testo (più propriamente una stringa), un numero o un valore logico. Per questo motivo sono definiti anche atomici. In JavaScript tutti i valori che non sono un oggetto appartengono ai tipi primitivi:

    Gli oggetti sono invece entità complesse e strutturate che contengono collezioni di dati primitivi o anche altri oggetti annidati al loro interno. Gli oggetti sono mutabili, in altre parole sono tipi dinamici che possono essere modificati in ogni momento. Tutto quello che non è primitivo è un oggetto. Anche gli array sono un tipo di oggetto, anche una funzione è un oggetto che ha la particolare capacità di essere eseguito.

    Complessità

    La prima distinzione che salta all’occhio è quindi la complessità: semplici e atomici i primi, strutturati i secondi. Qui nascono i primi equivoci: ma se i primitivi sono dei tipi semplici perché l’esperienza ci insegna che hanno anch’essi proprietà e metodi?

    JavaScript in questo caso crea degli oggetti temporanei (object wrappers) in base al tipo di dato (volta per volta gli oggetti String, Number, BigInt, Boolean o Symbol) che eseguono una proprietà o un metodo sul dato primitivo e vengono distrutti dopo l’utilizzo. In questo modo il dato rimane atomico e continua ad occupare pochissimo spazio in memoria ma, al contempo, è possibile eseguire tante operazioni utili su di esso. La controprova è fornita dall’ultima riga di codice console.log(typeof a); che ci conferma che al termine di queste operazioni il tipo è sempre string.

    Solo i tipi undefined e null, che di fatto indicano un non valore, non hanno oggetti temporanei e pertanto non è possibile eseguire nessuna operazione su di essi, in un certo senso li possiamo considerare i più primitivi di tutti.

    Mutabilità

    La seconda distinzione è la mutabilità: immutabili i primi, mutabili i secondi. Anche in questo caso sorge qualche perplessità: non è forse possibile assegnare a una variabile una stringa differente ed eseguire operazioni sulla stringa modificandola?

    È importante però non confondere un dato primitivo con la variabile a cui è assegnato. La variabile può ricevere un nuovo valore, ma il nuovo valore sostituisce quello precedente che non può essere modificato nello stesso modo in cui si modificano oggetti, array e funzioni. Ogni operazione sui dati primitivi restituisce un nuovo valore. Diversamente da quanto accade con gli oggetti, JavaScript non fornisce metodi per modificare un tipo primitivo.

    Valore e riferimento

    Abbiamo affermato in apertura che in JavaScript il tipo di dato ha implicazioni importanti su come il dato stesso viene salvato, copiato e confrontato. Finora abbiamo parlato di complessità e mutabilità, con la terza distinzione si entra ancora più nel vivo delle differenze tra dati primitivi e oggetti.

    Abbiamo detto inoltre che gli oggetti che “avvolgono” (da qui il termine inglese wrapper: involucro, contenitore) i dati primitivi vengono distrutti al termine del loro utilizzo per limitare il più possibile l’impiego di memoria. La gestione della memoria ha molto a che fare con i concetti di valore e riferimento.

    I dati primitivi vengono salvati, copiati e confrontati per valore. Ciò significa che in let a = "abc", il valore “abc” viene salvato nella variabile a. La riga let b = a copia il valore “abc” in b. Se si cambia il valore di una delle due variabili suddette il valore dell’altra resta immutato perché entrambe posseggono la loro copia del valore. Se due variabili con valore identico vengono confrontate sono considerate uguali.

    Fin qui è tutto abbastanza intuitivo, ma non funziona allo stesso modo per gli oggetti.

    Gli oggetti, infatti, vengono salvati, copiati e confrontati per riferimento. La riga let a = {name: "Marcello"} salva il valore in una locazione di memoria e salva il riferimento a quella locazione di memoria nella variabile a. Non salva quindi il valore stesso ma il riferimento all’area di memoria che lo contiene. Similmente anche la riga let b = a non copia il valore ma il riferimento al medesimo valore della variabile a. In altre parole entrambe le variabili puntano ad una locazione di memoria che contiene il valore ma il valore non viene duplicato.

    Probabilmente qualcuno avrà già tratto le conclusioni:

    1. se si modifica il valore entrambe le variabili risulteranno aggiornate;
    2. se due variabili con valore identico vengono confrontate non sono considerate uguali: sono uguali se contengono il medesimo riferimento a un valore.

    Dietro le quinte di questo comportamento controintuitivo c’è chiaramente la necessità di una gestione ottimale della memoria perché gli oggetti possono assumere potenzialmente una struttura anche molto complessa.

    Clonazione di oggetti

    E se si avesse la necessità di copiare in un’altra variabile non il riferimento alla locazione di memoria di un oggetto ma il valore stesso dell’oggetto? Se si avesse la necessità di duplicare l’oggetto? Con i tipi primitivi è sufficiente l’operatore =, ma abbiamo constatato che con gli oggetti questo non è possibile.

    In questi casi è possibile usare il metodo Object.assign oppure l’operatore spread.

    Oggetti annidati

    Sia Object.assign sia l’operatore spread, tuttavia, non effettuano una clonazione profonda (deep cloning o deep copy) ma solo una copia superficiale (shallow copy). Nell’esempio precedente il nostro oggetto conteneva soltanto un valore primitivo (la stringa “Marcello”) ma se al suo posto (o in aggiunta ad esso) ci fossero stati anche oggetti annidati, avremmo raggiunto pienamente il nostro scopo, ovvero la clonazione totale e completa dell’oggetto? No e lo possiamo appurare con un esempio semplice.

    Cosa è appena accaduto? La copia superficiale si è dimostrata efficace con il valore primitivo della proprietà name ma ha copiato soltanto il riferimento all’oggetto annidato hobbies (abbiamo detto prima che anche un array è un oggetto).

    È evidente che lavorare con JavaScript ad un’applicazione mediamente complessa, e non essere a conoscenza di un comportamento del genere, può dar luogo ad effetti indesiderati e conseguenti mal di testa. Come dovremmo modificare allora la riga let b = { …a };? Ci sono vari modi per effettuare una copia profonda di un oggetto. Vediamo alcune alternative ricordando che esistono anche librerie esterne che possono essere utilizzate allo scopo come Immer, ma in questo caso non le prenderemo in considerazione.

    L’aggiornamento dello stato nei moderni framework

    Alla luce di quanto detto non stupisce che le linee guida dei moderni framework come React prescrivano di considerare lo stato in sola lettura e di non modificarlo direttamente bensì di rimpiazzarlo.

    However, although objects in React state are technically mutable, you should treat them as if they were immutable – like numbers, booleans, and strings. Instead of mutating them, you should always replace them.

    react.dev

    Lo stato, infatti, il più delle volte non è altro che un oggetto la cui modifica ha effetti diretti nella UI dell’applicazione e non sfugge alle leggi che governano il funzionamento del linguaggio JavaScript che sottende ai framework.

    Sitografia

    La rete contiene tante ottime risorse che trattano l’argomento in modo esauriente e approfondito, ecco quelle più significative che ho consultato.