Articolo originale: https://www.freecodecamp.org/news/scope-and-closures-in-javascript/

Potresti aver visto o aver scritto alcune volte del codice come questo in JavaScript:

function sayWord(word) {
	return () => console.log(word);
}

const sayHello = sayWord("hello");

sayHello(); // "hello"

Questo codice è interessante per un paio di motivi. Primo, possiamo accedere a word nella funzione restituita da sayWord. Secondo, possiamo accedere al valore di word quando chiamiamo sayHello – anche se chiamiamo sayHello da dove normalmente non avremmo accesso a word.

In questo articolo, apprenderemo cosa sono lo scope (ambito di visibilità) e le closure (chiusure), che consentono questo tipo di comportamento.

Introduzione all'Ambito di Visibilità in JavaScript

L'ambito di visibilità è il primo pezzo che ci aiuterà a comprendere l'esempio precedente. L'ambito di visibilità di una variabile è la parte di un programma in cui la variabile è accessibile.

Le variabili in JavaScript hanno un ambito di visibilità lessicale, ossia possiamo determinare l'ambito di visibilità di una variabile in base a dove è dichiarata nel codice (ciò non è del tutto esatto: le variabili dichiarate con var non hanno un ambito di visibilità lessicale, ma ne riparleremo a breve).

Per esempio:

if (true) {
	const foo = "foo";
	console.log(foo); // "foo"
}

L'istruzione if introduce un ambito di blocco tramite una dichiarazione di blocco. Diciamo che foo ha un "ambito di visibilità di blocco" relativo all'istruzione if. Ciò significa che è accessibile solo dal codice dentro il blocco if.

Se provassimo ad accedere alla variabile foo dall'esterno di questo blocco, otterremmo un ReferenceError perché la variabile è fuori dall'attuale ambito di visibilità:

if (true) {
	const foo = "foo";
	console.log(foo); // "foo"
}

console.log(foo); // Uncaught ReferenceError: foo is not defined

Anche le altre istruzioni di blocco, come i loop for e while, creano un ambito di visibilità di blocco per le variabili. Per esempio, foo ha un ambito di visibilità limitato al corpo della funzione di seguito:

function sayFoo() {
	const foo = "foo";
	console.log(foo);
}

sayFoo(); // "foo"

console.log(foo); // Uncaught ReferenceError: foo is not defined

Ambito di Visibilità Annidato e Funzioni

JavaScript permette blocchi annidati e di conseguenza ambiti di visibilità annidati. Questi ultimi creano un albero o catena di ambiti di visibilità.

Consideriamo il seguente codice, che annida molteplici istruzioni di blocco:

if (true) {
	const foo = "foo";
	console.log(foo); // "foo"

	if (true) {
		const bar = "bar";
		console.log(foo); // "foo"

		if (true) {
			console.log(foo, bar); // "foo bar"
		}
	}
}

JavaScript ci permette anche di annidare le funzioni:

function foo(bar) {
	function baz() {
		console.log(bar);
	}

	baz();
}

foo("bar"); // "bar"

Come previsto, possiamo accedere alle variabili dall'ambito di visibilità in cui sono state dichiarate. Ma possiamo anche accedere alle variabili dall'ambito di visibilità interno (l'ambito di visibilità creato all'interno dell'ambito di visibilità della variabile stessa). Ovvero possiamo accedere a una variabile dall'ambito di visibilità in cui è stata dichiarata e da ogni ambito interno ad esso.

Prima di andare avanti è meglio chiarire come varia questo meccanismo in base a come dichiariamo una variabile.

Ambito di visibilità di let, const e var in JavaScript

Possiamo creare una variabile con una dichiarazione let, const e var. Per let e const, l'ambito di visibilità di blocco funziona come abbiamo appena spiegato. Mentre var funziona in maniera differente.

let e const

let e const creano variabili con ambito di blocco. Quando sono dichiarate all'interno di un blocco, sono accessibili solo dall'interno di quel blocco. Questo comportamento è stato mostrato nell'esempio precedente:

if (true) {
	const foo = "foo";
	console.log(foo); // "foo"
}

console.log(foo); // Uncaught ReferenceError: foo is not defined

var

L'ambito di visibilità di variabili create con var corrisponde al più vicino ambito di funzione o all'ambito globale (che discuteremo fra breve). Non hanno un ambito di visibilità di blocco:

function foo() {
	if (true) {
		var foo = "foo";
	}
	console.log(foo);
}

foo(); // "foo"

var può creare situazioni confuse, e la includo in questo articolo solo per completezza. Quando è possibile è meglio usare let e const. Per il resto dell'articolo useremo solo variabili dichiarate con let e const.

Se sei interessato a come funziona esattamente var nell'esempio precedente, puoi trovare maggiori informazioni nel mio articolo sull'hoisting.

Ambito di visibilità Globale e di Modulo in JavaScript

Oltre all'ambito di blocco, le variabili possono avere anche un ambito di visibilità globale o di modulo.

In un browser, l'ambito globale è il livello più alto dello script. È la radice dell'albero di cui abbiamo parlato prima. Quindi, creare una variabile nell'ambito globale la rende accessibile da qualsiasi altro ambito:

<script>
	const foo = "foo";
</script>
<script>
	console.log(foo); // "foo"
		
	function bar() {
		if (true) {
			console.log(foo);
		}
	}

	bar(); // "foo"
</script>

Ogni modulo invece ha il proprio ambito di visibilità. Le variabili dichiarate in un modulo sono accessibili solo da dentro quel modulo - non sono globali:

<script type="module">
	const foo = "foo";
</script>
<script>
	console.log(foo); // Uncaught ReferenceError: foo is not defined
</script>

Chiusure in JavaScript

Adesso che abbiamo capito come funziona l'ambito di visibilità, possiamo tornare all'esempio che abbiamo visto nell'introduzione:

function sayWord(word) {
	return () => console.log(word);
}

const sayHello = sayWord("hello");

sayHello(); // "hello"

Ricorda che c'erano due cose interessanti in questo esempio:

  1. La funzione restituita da sayWord ha accesso al parametro word
  2. La funzione restituita mantiene il valore di word quando sayHello è chiamata fuori dall'ambito di visibilità di word

Il primo punto può essere spiegato dall'ambito lessicale: la funzione ritornata può accedere a word perché esiste nel suo ambito esterno.

Il secondo punto si spiega con le chiusure: una chiusura è una funzione combinata con riferimenti a variabili definite fuori dalla funzione stessa. Le chiusure mantengono i riferimenti alla variabile, e questo permette loro di accedere alle variabili fuori dal loro ambito. "Racchiudono" la funzione e le variabili nel proprio ambiente.

Esempi di Chiusure in JavaScript

Probabilmente hai già incontrato e usato le chiusure senza neanche accorgertene. Vediamo alcuni altri modi per usarle.

Callback

Nelle funzioni callback è abituale far riferimento a una variabile dichiarata fuori dalla funzione stessa. Per esempio:

function getCarsByMake(make) {
	return cars.filter(x => x.make === make);
}

make è accessibile all'interno della funzione callback per via dell'ambito lessicale e il valore di make è preservato anche quando la funzione anonima è chiamata da filter grazie alla chiusura.

Memorizzazione dello stato

Possiamo usare le chiusure per restituire un oggetto che memorizzi lo stato da una funzione. Consideriamo la funzione makePerson qui di seguito, che restituisce un oggetto capace di memorizzare e cambiare il valore di name:

function makePerson(name) {
	let _name = name;

	return {
		setName: (newName) => (_name = newName),
		getName: () => _name,
	};
}

const me = makePerson("Zach");
console.log(me.getName()); // "Zach"

me.setName("Zach Snoek");
console.log(me.getName()); // "Zach Snoek"

Questo esempio dimostra che quando creiamo una chiusura il valore delle variabili fuori dal suo ambito non viene congelato. La chiusura mantiene il riferimento a queste variabili per tutto il suo ciclo di vita.

Metodi privati

Se sei pratico di programmazione orientata agli oggetti, avrai notato che l'ultimo esempio ricorda una classe che memorizza lo stato privato ed espone metodi pubblici getter e setter. Possiamo estendere questo parallelismo con la programmazione orientata agli oggetti usando le chiusure per implementare i metodi privati:

function makePerson(name) {
	let _name = name;

	function privateSetName(newName) {
		_name = newName;
	}

	return {
		setName: (newName) => privateSetName(newName),
		getName: () => _name,
	};
}

privateSetName non è accessibile direttamente e può accedere alla proprietà privata _name tramite una chiusura.

Gestori di Eventi in React

Infine, le chiusure sono usate abitualmente nei gestori di eventi in React. Il componente Counter qui di seguito è una variazione dalla documentazione di React:

function Counter({ initialCount }) {
	const [count, setCount] = React.useState(initialCount);

	return (
		<>
			<button onClick={() => setCount(initialCount)}>Reset</button>
			<button onClick={() => setCount((prevCount) => prevCount - 1)}>
				-
			</button>
			<button onClick={() => setCount((prevCount) => prevCount + 1)}>
				+
			</button>
			<button onClick={() => alert(count)}>Show count</button>
		</>
	);
}

function App() {
	return <Counter initialCount={0} />;
}

Le chiusure permettono:

  • ai gestori dell'evento click dei bottoni reset, decrement e increment di accedere a setCount
  • al bottone reset di accedere a initialCount dalle proprietà di Counter
  • e al bottone “Show count” di visualizzare lo stato count.

Le Chiusure sono importanti anche in altre parti di React, come prop e hook. Questi temi non rientrano nell'ambito di questo articolo. Per saperne di più sul ruolo delle chiusure in React, raccomando la lettura di questo post di Kent C. Dodds o questo post di Dan Abramov.

Conclusione

L'ambito di visibilità è la parte di un programma da cui abbiamo accesso a una variabile. JavaScript ci permette di annidare gli ambiti di visibilità, e le variabili dichiarate in un ambito esterno sono accessibili da uno interno. Le variabili possono avere ambito di visibilità globale, di modulo o di blocco.

Una chiusura è una funzione che racchiude un riferimento a una variabile dichiarata in un ambito di visibilità esterno ad essa. Le chiusure permettono alle funzioni di mantenere una connessione con delle variabili esterne, anche fuori dall'ambito di visibilità delle variabili stesse.

Esistono molteplici usi per le chiusure, dalla creazione di strutture simili alle classi che memorizzano lo stato e implementano metodi privati, al passare funzioni callback ai gestori di eventi.

Connettiamoci

Se sei interessato ad altri articoli come questo, iscriviti alla mia newsletter e seguimi su LinkedIn e Twitter!

Ringraziamenti

Grazie a Bryan Smith per avermi dato dei feedback sulle bozze di questo post.

Foto di Karine Avetisyan su Unsplash.