De callbacks, event handlers, hogere-orde functies kunnen toegang krijgen tot outer scope variabelen dankzij closures. Closures zijn belangrijk in functioneel programmeren, en worden vaak gevraagd tijdens het JavaScript coderingsinterview.
Hoewel closures overal worden gebruikt, zijn ze moeilijk te begrijpen. Als je je “Aha!”-moment in het begrijpen van closures nog niet hebt gehad, dan is deze post voor jou.
Ik zal beginnen met de fundamentele termen: scope en lexical scope. Daarna, nadat je de basis hebt begrepen, heb je nog maar één stap nodig om closures eindelijk te begrijpen.
Voordat je begint, stel ik voor dat je de neiging weerstaat om de secties over scope en lexical scope over te slaan. Deze concepten zijn cruciaal voor closures, en als je ze goed begrijpt, wordt het idee van closure vanzelfsprekend.
1. De scope
Als je een variabele definieert, wil je dat die binnen bepaalde grenzen toegankelijk is. Bv. een result
variabele is logisch om te bestaan binnen een calculate()
functie, als een intern detail. Buiten de calculate()
, is de result
variabele nutteloos.
De toegankelijkheid van variabelen wordt beheerd door scope. Je bent vrij om de variabele te benaderen die binnen zijn bereik is gedefinieerd. Maar buiten dat bereik is de variabele ontoegankelijk.
In JavaScript wordt een bereik gemaakt door een functie of codeblok.
Laten we eens kijken hoe het bereik de beschikbaarheid van een variabele beïnvloedt count
. Deze variabele behoort tot een bereik dat is gemaakt door de functie foo()
:
function foo() { // The function scope let count = 0; console.log(count); // logs 0}foo();console.log(count); // ReferenceError: count is not defined
count
is vrij toegankelijk binnen het bereik van foo()
.
Buiten de foo()
scope is count
echter niet toegankelijk. Als u probeert count
van buitenaf toch te benaderen, gooit JavaScript ReferenceError: count is not defined
.
In JavaScript zegt het bereik: als je een variabele hebt gedefinieerd binnen een functie of codeblok, dan kun je deze variabele alleen binnen die functie of dat codeblok gebruiken. Het bovenstaande voorbeeld demonstreert dit gedrag.
Nu een algemene formulering:
De reikwijdte is een ruimtebeleid dat de toegankelijkheid van variabelen regelt.
Een onmiddellijke eigenschap doet zich voor: de scope isoleert variabelen. Dat is mooi, want verschillende scopes kunnen variabelen met dezelfde naam hebben.
Je kunt gemeenschappelijke variabelennamen (count
index
current
value
, etc) in verschillende scopes hergebruiken zonder dat er botsingen optreden.
foo()
en bar()
functie scopes hebben hun eigen, maar dezelfde naam, variabelen count
:
function foo() { // "foo" function scope let count = 0; console.log(count); // logs 0}function bar() { // "bar" function scope let count = 1; console.log(count); // logs 1}foo();bar();
count
variabelen uit foo()
en bar()
functiescopes botsen niet.
2. Scopes nesting
Laten we nog wat meer spelen met scopes, en de ene scope in de andere zetten.
De functie innerFunc()
is genest binnen een buitenste functie outerFunc()
.
Hoe zouden de 2 functie scopes met elkaar interageren? Kan ik de variabele outerVar
van outerFunc()
benaderen vanuit de innerFunc()
scope?
Laten we dat eens proberen in het voorbeeld:
function outerFunc() { // the outer scope let outerVar = 'I am outside!'; function innerFunc() { // the inner scope console.log(outerVar); // => logs "I am outside!" } innerFunc();}outerFunc();
Inderdaad, outerVar
variabele is toegankelijk binnen innerFunc()
scope. De variabelen van het buitenste bereik zijn toegankelijk binnen het binnenste bereik.
Nu weet je 2 interessante dingen:
- Scopes kunnen genest worden
- De variabelen van de outer scope zijn toegankelijk binnen de inner scope
3. De lexicale scope
Hoe begrijpt JavaScript dat outerVar
binnen innerFunc()
correspondeert met de variabele outerVar
van outerFunc()
?
Dat komt omdat JavaScript een scoping-mechanisme implementeert dat lexical scoping (of static scoping) wordt genoemd. Lexical scoping betekent dat de toegankelijkheid van variabelen wordt bepaald door de positie van de variabelen in de broncode binnen de nesting scopes.
Eenvoudiger gezegd, lexical scoping betekent dat je binnen de binnenste scope toegang hebt tot variabelen van de buitenste scopes.
Het wordt lexisch (of statisch) genoemd omdat de engine (op het moment van lexing) de nesting van scopes bepaalt door alleen maar naar de JavaScript broncode te kijken, zonder deze uit te voeren.
Hier ziet u hoe de engine het vorige stukje code begrijpt:
- Ik zie dat u een functie definieert
outerFunc()
die een variabele heeftouterVar
. Goed. - Binnen de
outerFunc()
, zie ik dat je een functieinnerFunc()
definieert. - Binnen de
innerFunc()
, zie ik een variabeleouterVar
zonder declaratie. Omdat ik lexical scoping gebruik, beschouw ik de variabeleouterVar
binneninnerFunc()
als dezelfde variabele alsouterVar
vanouterFunc()
.
Het gedistilleerde idee van de lexical scope:
De lexical scope bestaat uit buitenste scopes die statisch worden bepaald.
Voorbeeld:
const myGlobal = 0;function func() { const myVar = 1; console.log(myGlobal); // logs "0" function innerOfFunc() { const myInnerVar = 2; console.log(myVar, myGlobal); // logs "1 0" function innerOfInnerOfFunc() { console.log(myInnerVar, myVar, myGlobal); // logs "2 1 0" } innerOfInnerOfFunc(); } innerOfFunc();}func();
De lexicale scope van innerOfInnerOfFunc()
bestaat uit scopes van innerOfFunc()
func()
en globale scope (de buitenste scope). Binnen innerOfInnerOfFunc()
heeft u toegang tot de lexical scope variabelen myInnerVar
myVar
en myGlobal
.
Het lexicale bereik van innerFunc()
bestaat uit func()
en globaal bereik. Binnen innerOfFunc()
kunt u de lexical scope variabelen myVar
en myGlobal
benaderen.
Ten slotte bestaat de lexical scope van func()
alleen uit de global scope. Binnen func()
heeft u toegang tot de lexical scope variabele myGlobal
.
4. De closure
Ok, de lexical scope maakt het mogelijk om statisch toegang te krijgen tot de variabelen van de outer scopes. Er is nog maar één stap tot de closure!
Laten we nog eens kijken naar het outerFunc()
en innerFunc()
voorbeeld:
function outerFunc() { let outerVar = 'I am outside!'; function innerFunc() { console.log(outerVar); // => logs "I am outside!" } innerFunc();}outerFunc();
Binnen de innerFunc()
scope wordt de variabele outerVar
benaderd vanuit de lexical scope. Dat is al bekend.
Merk op dat innerFunc()
aanroeping gebeurt binnen zijn lexische scope (de scope van outerFunc()
).
Laten we een wijziging aanbrengen: innerFunc()
aan te roepen buiten zijn lexicale bereik (buiten outerFunc()
). Zou innerFunc()
nog steeds toegang kunnen krijgen tot outerVar
?
Laten we de aanpassingen in de code snippet doorvoeren:
function outerFunc() { let outerVar = 'I am outside!'; function innerFunc() { console.log(outerVar); // => logs "I am outside!" } return innerFunc;}const myInnerFunc = outerFunc();myInnerFunc();
Nu wordt innerFunc()
uitgevoerd buiten zijn lexicale bereik. En wat belangrijk is:
innerFunc()
heeft nog steeds toegang tot outerVar
van zijn lexicale bereik, zelfs als het buiten zijn lexicale bereik wordt uitgevoerd.
Met andere woorden, innerFunc()
sluit over (a.k.a. vangt, onthoudt) de variabele outerVar
van zijn lexicale bereik.
Met andere woorden, innerFunc()
is een closure omdat het de variabele outerVar
uit zijn lexicale bereik oversluit.
Je hebt de laatste stap gezet om te begrijpen wat een closure is:
De closure is een functie die toegang heeft tot zijn lexicale bereik, zelfs als deze buiten zijn lexicale bereik wordt uitgevoerd.
Simpeler gezegd, de closure is een functie die de variabelen onthoudt van de plaats waar hij is gedefinieerd, ongeacht waar hij later wordt uitgevoerd.
Een vuistregel om een closure te herkennen: als je in een functie een vreemde variabele ziet (die niet in de functie is gedefinieerd), is die functie hoogstwaarschijnlijk een closure, omdat de vreemde variabele wordt opgevangen.
In de vorige codefragment, outerVar
is een alien variabele binnen de closure innerFunc()
gevangen uit outerFunc()
scope.
Laten we verder gaan met voorbeelden die laten zien waarom de closure nuttig is.
5. Voorbeelden van closure
5.1 Event handler
Laten we eens weergeven hoe vaak er op een knop is geklikt:
let countClicked = 0;myButton.addEventListener('click', function handleClick() { countClicked++; myText.innerText = `You clicked ${countClicked} times`;});
Open de demo en klik op de knop. De tekst wordt bijgewerkt om het aantal klikken weer te geven.
Wanneer op de knop wordt geklikt, wordt handleClick()
ergens binnen de DOM-code uitgevoerd. De uitvoering gebeurt ver van de plaats van definitie.
Maar omdat het een closure is, vangt handleClick()
countClicked
op uit de lexical scope en werkt het bij wanneer er op wordt geklikt. Sterker nog, myText
wordt ook opgevangen.
5.2 Callbacks
Het afvangen van variabelen uit de lexical scope is nuttig in callbacks.
Een setTimeout()
callback:
const message = 'Hello, World!';setTimeout(function callback() { console.log(message); // logs "Hello, World!"}, 1000);
De callback()
is een closure omdat het de variabele message
afvangt.
Een iteratorfunctie voor forEach()
:
let countEven = 0;const items = ;items.forEach(function iterator(number) { if (number % 2 === 0) { countEven++; }});countEven; // => 2
De iterator
is een closure omdat deze de variabele countEven
vangt.
5.3 Functioneel programmeren
Currying gebeurt wanneer een functie een andere functie retourneert totdat de argumenten volledig zijn aangeleverd.
Voorbeeld:
function multiply(a) { return function executeMultiply(b) { return a * b; }}const double = multiply(2);double(3); // => 6double(5); // => 10const triple = multiply(3);triple(4); // => 12
multiply
is een curried functie die een andere functie retourneert.
Currying, een belangrijk concept van functioneel programmeren, is ook mogelijk dankzij closures.
executeMultiply(b)
is een closure die a
uit zijn lexicale bereik haalt. Wanneer de closure wordt aangeroepen, worden de gevangen variabele a
en de parameter b
gebruikt om a * b
te berekenen.
6. Conclusie
Het bereik is wat de toegankelijkheid van variabelen in JavaScript regelt. Er kan een functie of een blok scope zijn.
De lexicale scope staat een functie scope toe om statisch toegang te krijgen tot de variabelen van de buitenste scopes.
Eindelijk is een closure een functie die variabelen van zijn lexical scope opvangt. In eenvoudige woorden, de closure onthoudt de variabelen van de plaats waar hij is gedefinieerd, ongeacht waar hij wordt uitgevoerd.
Closures vangen variabelen op in event handlers, callbacks. Ze worden gebruikt in functioneel programmeren. Bovendien zou je tijdens een sollicitatiegesprek voor Frontend kunnen worden gevraagd hoe closures werken.
Iedere JavaScript ontwikkelaar moet weten hoe closures werken. Deal with it ⌐■■.
Wat dacht je van een uitdaging? 7 Interview Vragen over JavaScript Closures. Kun je ze beantwoorden?