Closures
- functies data zijn;
- het bereik van variabelen op functie niveau is;
- functies een functie kunnen retourneren;
Bronnen
Zell Liew, JavaScript Scope and Closures, August 28, 2017
De bereik-ketting
In het Engels heet dat scope chain.
We hebben al gezien dat er in JavaScript geen codeblok bereik is maar een functiebereik (Bereik van variabelen). Een variabele, die gedefinieerd is binnenin een functie is niet zichtbaar buiten die functie, maar een variabele die in een codeblok, bijvoorbeeld een if of for lus, is gedefinieerd is wel zichtbaar buiten die codeblok:
var a = 1; function mijnFunctie() { var b = 1; return a; } mijnFunctie(); 1
Maar als we vervolgens vragen naar de waarde b krijgen we:
b; b is niet gedefinieerd
Let erop dat de JavaScript console in IE11 het antwoord netjes in het Nederlands heeft vertaald.
De variabele a
is in de globale ruimte, terwijl b
in het bereik van de functie mijnFunctie()
zit. Dus:
- binnenin
mijnFunctie()
, zijn zowela
enb
zichtbaar; - buiten
mijnFunction()
, isa
zichtbaar, maarb
niet;
Als je een functie inner() definieert binnen outer(), zal inner() toegang hebben tot variabelen binnen haar eigen bereik, en binnen het bereik van haar 'ouders'. Dat heet de bereikketting (scope chain), en de ketting kan zolang (deep) worden als het nodig is:
var global = 1; function outer() { var outer_local = 2; function inner() { var inner_local = 3; return inner_local + outer_local + global; } return inner(); }
Als we de functie uitvoeren zien we dat inner() toegang heeft tot alle variabelen:
outer(); 6
De ketting breken met een afsluiting
We gaan een closure stap voor stap uitleggen. We beginnen met de volgende code:
var a = "global variable"; var outer = function () { var b = "lokale variabele in outer"; var inner = function () { var c = "lokale variabele in inner"; }; };
Laten we de bereiken met een venndiagram voorstellen:
- A: de variabele
a
en de functieouter
zitten in de globale ruimte; - B: binnenin de functie
outer
heb je toegang tot de global ruimte en de lokale ruimte van deouter
functie. - C: Binnenin de functie
inner
heb je toegang tot de lokale ruimte van deinner
functie, de lokale ruimte van deouter
functie en tot de globale ruimte.
Je kan vanuit het bereik A niet aan het bereik van B. Je kan de variabele b bijvoorbeeld niet aanspreken in het A bereik. Het A en B bereik kunnen ook niet aan het bereik van C.
Maar je kan aan het bereik A vanuit het bereik B als je dat wilt. Je kan de variabele a aanspreken vanuit het B bereik. En vanuit het bereik C kan je aan het bereik B en A.
En nu wordt het interessant. Wat gebeurt er als de functie inner uitbreekt uit de outer functie en terecht komt in de globale ruimte?
De functie inner
is nu in dezelfde naamruimte (namespace) als de variabele a
. Vermits functies zich de plaats herinneren waar ze werden gedefinieerd, heeft de functie inner nog altijd toegang tot het B bereik en heeft ze dus toegang tot de variabele b
.
Dat is erg interessant. De inner functie zit waar de variabele zit en toch heeft de inner
functie toegang tot de variabele b
, terwijl de variabele a
dat niet heeft.
Hoe kan de inner
functie de ketting breken? Dat kan ze op twee manieren:
- door zichzelf globaal te maken en de
var
niet te gebruiken in de declaratie; - of door de
inner
functie te laten retourneren naar de globale ruimte;
Closure van de eerste soort
In de volgende code retourneert de functie outer
een referentie naar de inner functie en de inner
functie retourneert ook de variabele b
:
var a = "global variable"; var outer = function () { var b = "local variable"; var inner = function () { var c = "inner local"; return b; }; return inner; };
De outer
functie bevat de variabele b
, die lokaal is, en dus niet toegankelijk vanuit de globale ruimte. Als je vraagt wat de variabele b
is, krijg je als antwoord:
b; b is niet gedefinieerd
De outer
functie heeft toegang tot haar privéruimte, tot de outer
functieruimte en tot de globale ruimte. Dus ziet ze de variabele b
. Vermits de functie outer
kan opgeroepen worden vanuit de globale ruimte kan je ze oproepen in de globale ruimte en de retourwaarde toekennen aan een andere globale variabele. Het resultaat is een nieuwe globale functie die toegang heeft tot de privéruimte van de outer
functie:var
var global_inner = outer(); global_inner(); "local variable"
Closure van de tweede soort
Je kan het vorige nog op andere manier doen. De outer
functie retourneert geen functie maar creëert een nieuwe globale variabele met de naam global_inner
in haar codeblok.
Daarvoor declareer je eerst een plaats houder voor de toekomstige globale functie. Dat is niet echt nodig maar het is een goede gewoonte om je variabelen altijd te declareren. Daarna definieer je de outer
functie:
var outer = function () { var b = "local variable"; var inner = function () { return b; }; global_inner = inner; };
Wat gebeurt er als je de outer
functie uitvoert? Een nieuwe functie met de naam inner
wordt binnenin de functie outer
gedefinieerd en toegewezen aan de globale variabele global_inner
. Gedurende de definitietijd was de inner
functie binnenin de outer
functie en had dus toegang tot het bereik van de outer
functie. De functie global_inner
behoudt de toegang tot het bereik van de outer
functie ofschoon ze nu deel uitmaakt van de globale ruimte:
global_inner(); "local variable"
Closure van de derde soort
Elke functie kan worden beschouwd als een sluiting. Dit is omdat elke functie een 'geheime' koppeling met de context (het bereik ) onderhoudt) waarin ze gemaakt werd. Maar meestal wordt deze scope vernietigd tenzij er iets interessant gebeurt (zoals hierboven) die ervoor zorgt dat deze scope gehandhaafd wordt.
Je kan zeggen dat een sluiting wordt gemaakt wanneer een functie een link naar het 'ouder' bereik behoudt zelfs nadat de 'ouder' is uitgevoerd en heeft geretourneerd.
En elke functie is een sluiting omdat elke functie op zijn minst de toegang behoudt tot de globale scope, die nooit vernietigd wordt.
Nog een voorbeeld van een sluiting, deze keer met behulp van de functieparameters. Functieparameters gedragen zich als lokale variabelen binnenin deze functie, maar ze zijn impliciet gemaakt. Dat wil zeggen dat je het sleutelwoord var voor parameters niet moet gebruiken. Je kan een een functie maken die een andere functie retourneert, die op haar beurt de parameter van de 'ouder' retourneert:
function outer(parameter) { var inner = function () { return parameter; }; parameter++; return inner; }
Je gebruikt die functie als volgt:
var global_inner = outer(999); global_inner(); 1000
De parameter werd met de instructie parameter++
verhoogd nadat de functie is gedefinieerd. Maar wanneer global_inner
werd aangeroepen werd met de increment tijdens de declaratie geen rekening gehouden en keert global_inner()
de waarde bepaald in de global scope, namelijk 999, verhoogd met 1 terug. Dit toont aan dat de functie de verwijzing naar het bereik waar het werd gedefinieerd behoudt en niet naar de variabelen en hun waarden die gecreëerd werden tijdens de definitie van de functie.
Sluitingen: referenties en waarden
Het gebruik van sluitingen kan gemakkelijk leiden tot moeilijk te vinden bugs als je het verschil niet kent tussen een referentie en een value.
Om te tonen waarover dit gaat, lopen we driemaal door een lus waarbij bij iedere iteratie een nieuwe functie wordt gemaakt die de index van de lus retourneert. De nieuwe functies worden toegevoegd aan een array en de arraywordt geretourneerd op het einde. Hier is de functie:
function outer() { var local_array = [], i; for (i = 0; i < 3; i++) { local_array[i] = function () { return i; }; } return local_array; } var global_array = outer(); global_array[0](); global_array[1](); global_array[2](); 3
We voeren de functie uit en kennen de retourwaarde toe aan de variabele global_array:
var global_array = outer();
In de global_array
zitten nu drie functies. We roepen elk van deze drie functies op door ronde haken toe te voegen op het einde van elk element:
global_array[0](); 3 global_array[1](); 3 global_array[2](); 3
Het resultaat is niet echt wat we hadden verwacht. Alle drie de functies retourneren 3 en niet de index van de plaats waar ze in de array zitten. De index van de loop op het moment dat de functies in de array werden gestopt.
Hoe komt dat?
Alle drie de functies verwijzen naar de dezelfde lokale variabele i. Wat houdt dat in? Welnu functies onthouden geen waarden, ze onthouden enkel en alleen een verwijzing naar de context (bereik) waarin ze werden gecreëerd. In dit geval hier, leeft de variabele i in dezelfde context waarin ook de drie functies werden gemaakt. Als de functies de waarde retourneren van de variabele i is dat niet de waarde van i tijdens het maken van de drie functies, maar de waarde waarnaar i verwijst op het moment van het uitvoeren van de drie functies. Op het moment dat de drie functies worden uitgevoerd en de waarde van de variabele i wordt geretourneerd is de lus al volledig doorlopen en is de eindwaarde van de variabele i gelijk aan 3. Dat is dan ook de waarde die elk van de drie functies retourneren. Het is de waarde van de variabele i tijdens het uitvoeren van de functies en niet de waarde van i tijdens het definiëren van de drie functies.
Hoe kunnen we dat verhelpen en toch de waarde van i tijdens het maken van de drie functies retourneren?
Het antwoord hierop is het gebruik van nog een ander sluiting:
function outer() { var local_array = [], i; for (i = 0; i < 3; i++) { local_array[i] = (function (x) { return function () { return x; }; }(i)); } return local_array; }
Nu krijg je wel het verwachte resultaat:
var global_array = outer(); global_array[0](); 0 global_array[1](); 1 global_array[2](); 2
Getter/Setter
Nog een voorbeelden van het gebruik van sluitingen, het maken van getter- en setterfuncties . Stel dat je een variabele hebt die een specifieke waarde of reeks van waarden moet bevatten.
Je wilt niet dat die variabele zomaar ergens in de code kan een waarde kan toegewezen krijgen. Je wilt dus deze variabele binnen een functie beschermen en daarvoor maak je in die functie twee extra functies:
- een om de waarde uit die variabele op te halen;
- een andere om er een waarde aan toe te kennen;
De functie die de waarde kan instellen bevat dan meestal een of andere logica - ook wel business logica genoemd - om een waarde te valideren voordat die aan de beschermde variabele wordt toegewezen. We houden de validatielogica heel eenvoudig omdat we de nadruk willen leggen op de werking van getters en setters. We kijken alleen of de waarde die zich aandient om toegewezen te worden een numerieke waarde is.
Je zet de getter- en de setterfunctie binnenin dezelfde functie als waarin de geheime variabele staat. Op die manier delen ze hetzelfde bereik:
var getValue, setValue; (function () { var secret = 0; getValue = function () { return secret; }; setValue = function (v) { if (typeof v === "number") { secret = v; } }; }());
Hier is de functie die de setter- en de getterfunctie bevat een onmiddellijke functie. De onmiddellijke functie definieert de functie setValue() en getValue() als globale functies, terwijl de variabele secret lokaal is en niet direct toegankelijk is:
setValue(999); 999 setValue('dit is geen getal'); getValue(); 0