Gli ultimi anni ci hanno dimostrato che lo sviluppo web sta cambiando. Con l'aggiunta di molte funzionalità e API ai browser, abbiamo dovuto utilizzarle nel modo giusto. Il linguaggio a cui dobbiamo questo onore è stato JavaScript.
Inizialmente, gli sviluppatori non erano convinti di come era stato progettato e hanno avuto impressioni per lo più negative durante l'utilizzo di questo script. Col tempo, si è scoperto che questo linguaggio ha un grande potenziale e i successivi standard ECMAScript rendono alcune meccaniche più umane e, semplicemente, migliori. In questo articolo ne esaminiamo alcuni.
Tipi di valori in JS
La ben nota verità su JavaScript è che qui tutto è un oggetto. Davvero, tutto: array, funzioni, stringhe, numeri e persino booleani. Tutti i tipi di valori sono rappresentati da oggetti e hanno i loro metodi e campi. Tuttavia, possiamo dividerli in due categorie: primitivi e strutturali. I valori della prima categoria sono immutabili, il che significa che possiamo riassegnare una variabile con il nuovo valore, ma non possiamo modificare il valore esistente. La seconda rappresenta valori che possono essere modificati, quindi devono essere interpretati come collezioni di proprietà che possiamo sostituire o semplicemente chiamare i metodi che sono progettati per farlo.
Ambito di applicazione delle variabili dichiarate
Prima di approfondire, spieghiamo il significato di scope. Possiamo dire che l'ambito è l'unica area in cui possiamo utilizzare le variabili dichiarate. Prima dello standard ES6, potevamo dichiarare le variabili con l'istruzione var e assegnare loro un ambito globale o locale. Il primo è un ambito che ci permette di accedere ad alcune variabili in qualsiasi punto dell'applicazione, il secondo è dedicato a un'area specifica, principalmente una funzione.
Dal momento che lo standard ES2015, JavaScript ha tre modi per dichiarare le variabili, che differiscono per la parola chiave. Il primo è già stato descritto in precedenza: le variabili dichiarate con la parola chiave var hanno lo scope del corpo della funzione corrente. Lo standard ES6 ci ha permesso di dichiarare le variabili con modalità più umane: al contrario delle dichiarazioni var, le variabili dichiarate con le dichiarazioni const e let hanno uno scope solo sul blocco. Tuttavia, JS tratta l'istruzione const in modo piuttosto insolito, se confrontata con altre dichiarazioni linguaggi di programmazione - invece di un valore persistente, mantiene un riferimento persistente al valore. In breve, possiamo modificare le proprietà di un oggetto dichiarato con un'istruzione const, ma non possiamo sovrascrivere il riferimento a questa variabile. Alcuni sostengono che l'alternativa var in ES6 sia in realtà l'istruzione let. No, non è così, e l'istruzione var non è e probabilmente non sarà mai ritirata. Una buona pratica è quella di evitare l'uso di dichiarazioni var, perché nella maggior parte dei casi ci danno più problemi. A sua volta, dobbiamo abusare delle dichiarazioni const, fino a quando non dobbiamo modificare il suo riferimento - allora dovremmo usare let.
Esempio di comportamento inatteso dell'ambito
Cominciamo con quanto segue codice:
(() => {
for (var i = 0; i {
console.log(`Valore di "i": ${i}`);
}, 1000);
}
})();
Se lo guardiamo, sembra che il ciclo for iteri il valore i e, dopo un secondo, registri i valori dell'iteratore: 1, 2, 3, 4, 5. Ma non è così. Come abbiamo detto sopra, l'istruzione var serve a mantenere il valore di una variabile per tutto il corpo della funzione; ciò significa che alla seconda, terza e così via iterazione il valore della variabile i verrà sostituito con un valore successivo. Infine, il ciclo termina e i ticchettii del timeout ci mostrano la seguente situazione: 5, 5, 5, 5, 5, 5. Il modo migliore per mantenere il valore corrente dell'iteratore è utilizzare l'istruzione let:
(() => {
for (let i = 0; i {
console.log(`Valore di "i": ${i}`);
}, 1000);
}
})();
Nell'esempio precedente, manteniamo l'ambito del valore i nel blocco di iterazione corrente; è l'unico ambito in cui possiamo usare questa variabile e nulla può sovrascriverla al di fuori di questo ambito. In questo caso, il risultato è quello atteso: 1 2 3 4 5. Vediamo come gestire questa situazione con una dichiarazione var:
(() => {
for (var i = 0; i {
setTimeout(() => {
console.log(`Valore di "j": ${j}`);
}, 1000);
})(i);
}
})();
Poiché l'istruzione var serve a mantenere il valore all'interno del blocco funzione, dobbiamo chiamare una funzione definita che accetta un argomento - il valore dello stato corrente dell'iteratore - e poi fare qualcosa. Nulla al di fuori della funzione dichiarata potrà sovrascrivere il valore di j.
Esempi di aspettative errate sui valori degli oggetti
Il crimine più frequente che ho notato riguarda l'ignorare il potere delle strutture e il cambiare le loro proprietà che vengono modificate anche in altri pezzi di codice. Date un'occhiata veloce:
const DEFAULT_VALUE = {
favoriteBand: 'The Weeknd'
};
const currentValue = DEFAULT_VALUE;
const bandInput = document.querySelector('#favorite-band');
const restoreDefaultButton = document.querySelector('#restore-button');
bandInput.addEventListener('input', () => {
currentValue.favoriteBand = bandInput.value;
}, false);
restoreDefaultButton.addEventListener('click', () => {
currentValue = DEFAULT_VALUE;
}, false);
Dall'inizio: supponiamo di avere un modello con proprietà predefinite, memorizzate come oggetto. Vogliamo avere un pulsante che ripristini i valori di input a quelli predefiniti. Dopo aver riempito l'input con alcuni valori, aggiorniamo il modello. Dopo un attimo, pensiamo che la scelta predefinita fosse semplicemente migliore, quindi vogliamo ripristinarla. Facciamo clic sul pulsante... e non succede nulla. Perché? Perché ignoriamo il potere dei valori di riferimento.
Questa parte: const currentValue = DEFAULTVALUE sta dicendo al JS quanto segue: prendere il riferimento al DEFAULTVALUE e assegnare la variabile currentValue. Il valore reale viene memorizzato una sola volta ed entrambe le variabili puntano ad esso. Modificare alcune proprietà in un punto significa modificarle in un altro. Esistono alcuni modi per evitare situazioni di questo tipo. Uno che soddisfa le nostre esigenze è l'operatore spread. Correggiamo il nostro codice:
const DEFAULT_VALUE = {
favoriteBand: 'The Weeknd'
};
const currentValue = { ...DEFAULT_VALUE };
const bandInput = document.querySelector('#favorite-band');
const restoreDefaultButton = document.querySelector('#restore-button');
bandInput.addEventListener('input', () => {
currentValue.favoriteBand = bandInput.value;
}, false);
restoreDefaultButton.addEventListener('click', () => {
currentValue = { ...DEFAULT_VALUE };
}, false);
In questo caso, l'operatore spread funziona in questo modo: prende tutte le proprietà di un oggetto e crea un nuovo oggetto riempito con esse. In questo modo, i valori in currentValue e DEFAULT_VALUE non puntano più allo stesso punto della memoria e tutte le modifiche applicate a uno di essi non influenzeranno gli altri.
Ok, quindi la domanda è: si tratta solo di usare il magico operatore di spread? In questo caso sì, ma i nostri modelli potrebbero richiedere una maggiore complessità rispetto a questo esempio. Nel caso in cui si utilizzino oggetti annidati, array o qualsiasi altra struttura, l'operatore spread del valore referenziato di primo livello avrà effetto solo sul livello superiore e le proprietà referenziate continueranno a condividere lo stesso posto in memoria. Ci sono molte soluzioni per gestire questo problema, tutto dipende dalle vostre esigenze. Possiamo clonare gli oggetti a ogni livello di profondità o, per operazioni più complesse, utilizzare strumenti come immer, che ci permettono di scrivere codice immutabile in modo quasi indolore.
Mescolare il tutto
È possibile utilizzare un mix di conoscenze sugli ambiti e sui tipi di valori? Certo che sì! Costruiamo qualcosa che li utilizzi entrambi:
const useValue = (defaultValue) => {
const value = [...defaultValue];
const setValue = (newValue) => {
value.length = 0; // un modo complicato per cancellare l'array
newValue.forEach((item, index) => {
valore[indice] = elemento;
});
// fare altre cose
};
restituire [valore, setValue];
};
const [animali, setAnimali] = useValue(['gatto', 'cane']);
console.log(animals); // ['gatto', 'cane']
setAnimals(['cavallo', 'mucca']);
console.log(animals); // ['cavallo', 'mucca']);
Spieghiamo come funziona questo codice riga per riga. La funzione useValue crea un array basato sull'argomento defaultValue; crea una variabile e un'altra funzione, il suo modificatore. Questo modificatore prende un nuovo valore che viene applicato in modo complicato a quello esistente. Al termine della funzione, restituiamo il valore e il suo modificatore come valori dell'array. Successivamente, utilizziamo la funzione creata: dichiariamo animals e setAnimals come valori restituiti. Utilizziamo il loro modificatore per verificare se la funzione influisce sulla variabile animale: sì, funziona!
Ma aspettate, cosa c'è esattamente di così sofisticato in questo codice? Il riferimento mantiene tutti i nuovi valori e si può iniettare la propria logica in questo modificatore, come ad esempio alcune API o una parte dell'ecosistema che alimenta il flusso di dati senza alcuno sforzo. Questo schema complicato è spesso usato nelle librerie JS più moderne, dove il paradigma funzionale della programmazione ci permette di mantenere il codice meno complesso e più facile da leggere per gli altri programmatori.
Sintesi
La comprensione di come funzionano le meccaniche del linguaggio sotto il cofano ci permette di scrivere codice più consapevole e leggero. Anche se JS non è un linguaggio di basso livello e ci obbliga ad avere una certa conoscenza di come viene assegnata e memorizzata la memoria, dobbiamo comunque tenere d'occhio i comportamenti inaspettati quando modifichiamo gli oggetti. D'altra parte, abusare dei cloni di valori non è sempre la strada giusta e un uso scorretto ha più svantaggi che vantaggi. Il modo giusto di pianificare il flusso di dati è considerare ciò di cui si ha bisogno e i possibili ostacoli che si possono incontrare quando si implementa la logica dell'applicazione.
Per saperne di più: