Closures
Home

Closures

Closures

Dat lijkt één van de moeilijkste aspecten van JavaScript te zijn. Closures zijn mogelijk omdat in JavaScript:
Closures bieden veel mogelijkheden omdat in JavaScript functies zichzelf kunnen wijzigen.

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:

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:

JS - closure vendiagram 1
JS - closure vendiagram 1

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?

JS - closure vendiagram 2
JS - closure vendiagram 2

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:

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:

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

JI
2017-09-02 10:46:54