Fermetures JavaScript vs fonctions anonymes

562

Un de mes amis et moi discutons actuellement de ce qui est une fermeture dans JS et ce qui ne l'est pas. Nous voulons juste nous assurer de bien le comprendre correctement.

Prenons cet exemple. Nous avons une boucle de comptage et voulons imprimer la variable de compteur sur la console différée. Par conséquent, nous utilisons setTimeoutet fermetures pour capturer la valeur de la variable compteur pour nous assurer qu'elle n'imprimera pas N fois la valeur N.

La mauvaise solution sans fermetures ou quoi que ce soit proche des fermetures serait:

for(var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i);
    }, 1000);
}

qui imprimera bien sûr 10 fois la valeur de iafter the loop, soit 10.

Donc sa tentative était:

for(var i = 0; i < 10; i++) {
    (function(){
        var i2 = i;
        setTimeout(function(){
            console.log(i2);
        }, 1000)
    })();
}

impression de 0 à 9 comme prévu.

Je lui ai dit qu'il n'utilisait pas de fermeture pour capturer i, mais il insiste sur le fait. J'ai prouvé qu'il n'utilise pas de fermetures en plaçant le corps de la boucle for dans un autre setTimeout(en passant sa fonction anonyme à setTimeout), en imprimant 10 fois 10 à nouveau. La même chose s'applique si je stocke sa fonction dans un varet l'exécute après la boucle, en imprimant également 10 fois 10. Donc mon argument est qu'il ne capture pas vraiment la valeur dei , ce qui rend sa version pas une fermeture.

Ma tentative était:

for(var i = 0; i < 10; i++) {
    setTimeout((function(i2){
        return function() {
            console.log(i2);
        }
    })(i), 1000);
}

Donc je capture i(nommé i2dans la fermeture), mais maintenant je retourne une autre fonction et je la passe. Dans mon cas, la fonction passée à setTimeout capture vraiment i.

Maintenant, qui utilise les fermetures et qui n'en utilise pas?

Notez que les deux solutions impriment de 0 à 9 sur la console différée, donc elles résolvent le problème d'origine, mais nous voulons comprendre laquelle de ces deux solutions utilise des fermetures pour y parvenir.

leèmes
la source
1
@leemes: Voir ma modification ninja pour un deuxième lien.
Blender
2
nous venons de conclure un accord: celui qui a raison va obtenir les points SO liés à cette question
brillout
1
@leemes - Vous utilisez tous les deux des fermetures. Vous avez tous deux créé deux fonctions - une fonction extérieure et une fonction intérieure; et vos deux fonctions internes sont des fermetures. Toutes vos fonctions sont des lambdas ( fonctions anonymes ). Lisez ma réponse pour les détails.
Aadit M Shah
1
@blesh - Je n'ai aucune idée de ce qu'est une fermeture modifiée. Je vois que votre lien pointe vers le code C #. Les fermetures modifiées sont-elles prises en charge par JavaScript?
Aadit M Shah

Réponses:

650

Note de l'éditeur: toutes les fonctions en JavaScript sont des fermetures comme expliqué dans cet article . Cependant, nous ne sommes intéressés qu'à identifier un sous-ensemble de ces fonctions qui sont intéressantes d'un point de vue théorique. Désormais, toute référence au mot fermeture fera référence à ce sous-ensemble de fonctions, sauf indication contraire.

Une explication simple pour les fermetures:

  1. Prenez une fonction. Appelons cela F.
  2. Énumérez toutes les variables de F.
  3. Les variables peuvent être de deux types:
    1. Variables locales (variables liées)
    2. Variables non locales (variables libres)
  4. Si F n'a pas de variables libres, il ne peut pas s'agir d'une fermeture.
  5. Si F a des variables libres (qui sont définies dans une portée parent de F) alors:
    1. Il ne doit y avoir qu'une seule portée parent de F à laquelle une variable libre est liée.
    2. Si F est référencé de l'extérieur de cette portée parent, il devient alors une fermeture pour cette variable libre.
    3. Cette variable libre est appelée une valeur à la hausse de la fermeture F.

Maintenant, utilisons-le pour déterminer qui utilise les fermetures et qui ne le fait pas (pour des raisons d'explication, j'ai nommé les fonctions):

Cas 1: le programme de votre ami

for (var i = 0; i < 10; i++) {
    (function f() {
        var i2 = i;
        setTimeout(function g() {
            console.log(i2);
        }, 1000);
    })();
}

Dans le programme ci-dessus, il y a deux fonctions: fet g. Voyons voir s'il s'agit de fermetures:

Pour f:

  1. Énumérez les variables:
    1. i2est une variable locale .
    2. iest une variable libre .
    3. setTimeoutest une variable libre .
    4. gest une variable locale .
    5. consoleest une variable libre .
  2. Recherchez la portée parent à laquelle chaque variable libre est liée:
    1. iest lié à la portée mondiale.
    2. setTimeoutest lié à la portée mondiale.
    3. consoleest lié à la portée mondiale.
  3. Dans quelle portée la fonction est-elle référencée ? La portée mondiale .
    1. Par conséquent, in'est pas fermé par f.
    2. Par conséquent, setTimeoutn'est pas fermé par f.
    3. Par conséquent, consolen'est pas fermé par f.

La fonction fn'est donc pas une fermeture.

Pour g:

  1. Énumérez les variables:
    1. consoleest une variable libre .
    2. i2est une variable libre .
  2. Recherchez la portée parent à laquelle chaque variable libre est liée:
    1. consoleest lié à la portée mondiale.
    2. i2est lié à la portée de f.
  3. Dans quelle portée la fonction est-elle référencée ? La portée desetTimeout .
    1. Par conséquent, consolen'est pas fermé par g.
    2. Par conséquent, i2est fermé par g.

Ainsi, la fonction gest une fermeture pour la variable libre i2(qui est une valeur positive pour g) lorsqu'elle est référencée de l'intérieur setTimeout.

Mauvais pour vous: votre ami utilise une fermeture. La fonction intérieure est une fermeture.

Cas 2: votre programme

for (var i = 0; i < 10; i++) {
    setTimeout((function f(i2) {
        return function g() {
            console.log(i2);
        };
    })(i), 1000);
}

Dans le programme ci-dessus, il y a deux fonctions: fet g. Voyons voir s'il s'agit de fermetures:

Pour f:

  1. Énumérez les variables:
    1. i2est une variable locale .
    2. gest une variable locale .
    3. consoleest une variable libre .
  2. Recherchez la portée parent à laquelle chaque variable libre est liée:
    1. consoleest lié à la portée mondiale.
  3. Dans quelle portée la fonction est-elle référencée ? La portée mondiale .
    1. Par conséquent, consolen'est pas fermé par f.

La fonction fn'est donc pas une fermeture.

Pour g:

  1. Énumérez les variables:
    1. consoleest une variable libre .
    2. i2est une variable libre .
  2. Recherchez la portée parent à laquelle chaque variable libre est liée:
    1. consoleest lié à la portée mondiale.
    2. i2est lié à la portée de f.
  3. Dans quelle portée la fonction est-elle référencée ? La portée desetTimeout .
    1. Par conséquent, consolen'est pas fermé par g.
    2. Par conséquent, i2est fermé par g.

Ainsi, la fonction gest une fermeture pour la variable libre i2(qui est une valeur positive pour g) lorsqu'elle est référencée de l'intérieur setTimeout.

Bon pour vous: vous utilisez une fermeture. La fonction intérieure est une fermeture.

Donc, vous et votre ami utilisez des fermetures. Arrêter de se disputer. J'espère avoir clarifié le concept des fermetures et comment les identifier pour vous deux.

Edit: Une explication simple de la raison pour laquelle toutes les fonctions sont fermées (crédits @Peter):

Considérons d'abord le programme suivant (c'est le contrôle ):

lexicalScope();

function lexicalScope() {
    var message = "This is the control. You should be able to see this message being alerted.";

    regularFunction();

    function regularFunction() {
        alert(eval("message"));
    }
}

  1. Nous savons que les deux lexicalScopeet regularFunctionne sont pas des fermetures de la définition ci-dessus .
  2. Lorsque nous exécutons le programme, nous nous attendons message à être alertés car ce regularFunction n'est pas une fermeture (c'est-à-dire qu'il a accès à toutes les variables dans sa portée parent - y compris message).
  3. Lorsque nous exécutons le programme, nous constatons qu'il messageest en effet alerté.

Considérons maintenant le programme suivant (c'est l' alternative ):

var closureFunction = lexicalScope();

closureFunction();

function lexicalScope() {
    var message = "This is the alternative. If you see this message being alerted then in means that every function in JavaScript is a closure.";

    return function closureFunction() {
        alert(eval("message"));
    };
}

  1. Nous savons que ce closureFunctionn'est qu'une clôture de la définition ci-dessus .
  2. Lorsque nous exécutons le programme, nous nous attendons à message ne pas être alerté car il closureFunction s'agit d'une fermeture (c'est-à-dire qu'il n'a accès qu'à toutes ses variables non locales au moment de la création de la fonction ( voir cette réponse ) - cela n'inclut pas message).
  3. Lorsque nous exécutons le programme, nous observons qu'il messageest effectivement alerté.

Qu'en déduisons-nous?

  1. Les interprètes JavaScript ne traitent pas les fermetures différemment de la façon dont ils traitent les autres fonctions.
  2. Chaque fonction porte sa chaîne de portée avec elle. Les fermetures n'ont pas d' environnement de référencement séparé .
  3. Une fermeture est comme toutes les autres fonctions. Nous les appelons simplement fermetures lorsqu'elles sont référencées dans un domaine hors du domaine auquel elles appartiennent car il s'agit d'un cas intéressant.
Aadit M Shah
la source
40
Accepté parce que vous allez beaucoup dans les détails, expliquant très bien ce qui se passe. Et enfin, j'ai maintenant mieux compris ce qu'est une fermeture, ou mieux dit: comment fonctionne la liaison de variables dans JS.
leemes
3
Dans le cas 1, vous dites que gs'exécute dans la portée de setTimeout, mais dans le cas 2, vous dites que fs'exécute dans la portée globale. Ils sont tous les deux dans setTimeout, alors quelle est la différence?
rosscj2533
9
Pourriez-vous indiquer vos sources à cet effet? Je n'ai jamais vu de définition où une fonction pourrait être une fermeture si elle était appelée dans une portée mais pas dans une autre. Ainsi, cette définition semble être un sous-ensemble de la définition plus générale à laquelle je suis habitué (voir la réponse de kev ) où une fermeture est une fermeture est une fermeture quelle que soit la portée qu'elle est appelée, ou même si elle n'est jamais appelée!
Briguy37
11
@AaditMShah Je suis d'accord avec vous sur ce qu'est une fermeture, mais vous parlez comme s'il y avait une différence entre les fonctions régulières et les fermetures en JavaScript. Il n'y a pas de différence; en interne, chaque fonction comporte une référence à la chaîne de portée particulière dans laquelle elle a été créée. Le moteur JS ne le considère pas comme un cas différent. Il n'est pas nécessaire d'avoir une liste de contrôle compliquée; sachez que chaque objet fonction a une portée lexicale. Le fait que les variables / propriétés soient disponibles globalement ne fait pas moins de la fonction une fermeture (c'est juste un cas inutile).
Peter
13
@Peter - Vous savez quoi, vous avez raison. Il n'y a pas de différence entre une fonction régulière et une fermeture. J'ai effectué un test pour le prouver et cela se traduit en votre faveur: voici le contrôle et voici l' alternative . Ce que vous dites a du sens. L'interpréteur JavaScript doit effectuer une comptabilité spéciale pour les fermetures. Ce sont simplement des sous-produits d'un langage à portée lexicale avec des fonctions de première classe. Ma connaissance se limitait à ce que je lisais (ce qui était faux). Merci de m'avoir corrigé. Je mettrai à jour ma réponse pour refléter la même chose.
Aadit M Shah
96

Selon la closuredéfinition:

Une "fermeture" est une expression (généralement une fonction) qui peut avoir des variables libres avec un environnement qui lie ces variables (qui "ferme" l'expression).

Vous utilisez closuresi vous définissez une fonction qui utilise une variable définie en dehors de la fonction. (nous appelons la variable une variable libre ).
Ils utilisent tous closure(même dans le 1er exemple).

kev
la source
1
Comment la troisième version utilise-t-elle une variable définie en dehors de la fonction?
Jon
1
@Jon l'utilisation de la fonction retournée i2qui est définie à l'extérieur.
kev
1
@kev Vous utilisez la fermeture si vous définissez une fonction qui utilise une variable qui est définie en dehors de la fonction ...... puis dans "Cas 1: Programme de votre ami" de "Aadit M Shah" la réponse est "fonction f" une fermeture? il utilise le i (variable qui est définie en dehors de la fonction). la portée mondiale fait-elle référence à un déterminant?
internals-in
54

En un mot Javascript Closures permettent une fonction d' accéder à une variable qui est déclarée dans une fonction-parent lexical .

Voyons une explication plus détaillée. Pour comprendre les fermetures, il est important de comprendre comment JavaScript étend les variables.

Portées

En JavaScript, les étendues sont définies avec des fonctions. Chaque fonction définit une nouvelle portée.

Prenons l'exemple suivant;

function f()
{//begin of scope f
  var foo='hello'; //foo is declared in scope f
  for(var i=0;i<2;i++){//i is declared in scope f
     //the for loop is not a function, therefore we are still in scope f
     var bar = 'Am I accessible?';//bar is declared in scope f
     console.log(foo);
  }
  console.log(i);
  console.log(bar);
}//end of scope f

appeler f imprime

hello
hello
2
Am I Accessible?

Considérons maintenant le cas où nous avons une fonction gdéfinie dans une autre fonction f.

function f()
{//begin of scope f
  function g()
  {//being of scope g
    /*...*/
  }//end of scope g
  /*...*/
}//end of scope f

Nous appellerons fle parent lexical de g. Comme expliqué précédemment, nous avons maintenant 2 portées; la portée fet la portée g.

Mais une portée est "dans" l'autre portée, donc la portée de la fonction enfant fait-elle partie de la portée de la fonction parent? Que se passe-t-il avec les variables déclarées dans la portée de la fonction parent; pourrai-je y accéder depuis le champ de la fonction enfant? C'est exactement là que les fermetures interviennent.

Fermetures

En JavaScript, la fonction gpeut non seulement accéder à toutes les variables déclarées dans la portée, gmais également accéder à toutes les variables déclarées dans la portée de la fonction parent f.

Envisagez de suivre;

function f()//lexical parent function
{//begin of scope f
  var foo='hello'; //foo declared in scope f
  function g()
  {//being of scope g
    var bar='bla'; //bar declared in scope g
    console.log(foo);
  }//end of scope g
  g();
  console.log(bar);
}//end of scope f

appeler f imprime

hello
undefined

Regardons la ligne console.log(foo);. À ce stade, nous sommes dans la portée get nous essayons d'accéder à la variable foodéclarée dans la portée f. Mais comme indiqué précédemment, nous pouvons accéder à n'importe quelle variable déclarée dans une fonction parent lexicale, ce qui est le cas ici; gest le parent lexical de f. Par conséquent, helloest imprimé.
Regardons maintenant la ligne console.log(bar);. À ce stade, nous sommes dans la portée fet nous essayons d'accéder à la variable bardéclarée dans la portée g. barn'est pas déclaré dans la portée actuelle et la fonction gn'est pas le parent de f, barn'est donc pas définie

En fait, nous pouvons également accéder aux variables déclarées dans le cadre d'une fonction lexicale de «grand parent». Par conséquent, s'il y avait une fonction hdéfinie dans la fonctiong

function f()
{//begin of scope f
  function g()
  {//being of scope g
    function h()
    {//being of scope h
      /*...*/
    }//end of scope h
    /*...*/
  }//end of scope g
  /*...*/
}//end of scope f

alors hserait en mesure d'accéder à toutes les variables déclarées dans le cadre de la fonction h, get f. Cela se fait avec des fermetures . Dans les fermetures JavaScript, nous pouvons accéder à toute variable déclarée dans la fonction parent lexicale, dans la fonction grand parent lexical, dans la fonction grand-grand parent lexical, etc. Cela peut être vu comme une chaîne de portée ; scope of current function -> scope of lexical parent function -> scope of lexical grand parent function -> ... jusqu'à la dernière fonction parent qui n'a pas de parent lexical.

L'objet fenêtre

En fait, la chaîne ne s'arrête pas à la dernière fonction parent. Il y a une autre portée spéciale; la portée mondiale . Chaque variable non déclarée dans une fonction est considérée comme déclarée dans la portée globale. La portée mondiale a deux spécialités;

  • chaque variable déclarée dans la portée globale est accessible partout
  • les variables déclarées dans la portée globale correspondent aux propriétés de l' windowobjet.

Par conséquent, il existe exactement deux façons de déclarer une variable foodans la portée globale; soit en ne le déclarant pas dans une fonction, soit en définissant la propriété foode l'objet window.

Les deux tentatives utilisent des fermetures

Maintenant que vous avez lu une explication plus détaillée, il peut maintenant être évident que les deux solutions utilisent des fermetures. Mais pour être sûr, faisons une preuve.

Créons un nouveau langage de programmation; JavaScript sans fermeture. Comme son nom l'indique, JavaScript-No-Closure est identique à JavaScript, sauf qu'il ne prend pas en charge les fermetures.

En d'autres termes;

var foo = 'hello';
function f(){console.log(foo)};
f();
//JavaScript-No-Closure prints undefined
//JavaSript prints hello

Bon, voyons ce qui se passe avec la première solution avec JavaScript-No-Closure;

for(var i = 0; i < 10; i++) {
  (function(){
    var i2 = i;
    setTimeout(function(){
        console.log(i2); //i2 is undefined in JavaScript-No-Closure 
    }, 1000)
  })();
}

par conséquent, cela s'imprimera undefined10 fois en JavaScript sans fermeture.

Par conséquent, la première solution utilise la fermeture.

Regardons la deuxième solution;

for(var i = 0; i < 10; i++) {
  setTimeout((function(i2){
    return function() {
        console.log(i2); //i2 is undefined in JavaScript-No-Closure
    }
  })(i), 1000);
}

par conséquent, cela s'imprimera undefined10 fois en JavaScript sans fermeture.

Les deux solutions utilisent des fermetures.

Modifier: il est supposé que ces 3 extraits de code ne sont pas définis dans la portée globale. Sinon, les variables fooet iseraient liées à l' windowobjet et donc accessibles via l' windowobjet en JavaScript et JavaScript-No-Closure.

brillout.com
la source
Pourquoi ne devrait-il ipas être défini? Vous faites simplement référence à la portée parent, qui est toujours valide s'il n'y a pas eu de fermeture.
leemes
pour la même raison que foo n'est pas défini dans JavaScript-No-Closure. <code> i </code> n'est pas indéfini en JavaScript grâce à une fonctionnalité en JavaScript qui permet d'accéder aux variables définies dans le parent lexical. Cette fonction est appelée fermeture.
brillout
Vous n'avez pas compris la différence entre se référer à des variables déjà définies et des variables libres . Dans les fermetures, nous définissons des variables libres qui doivent être liées dans le contexte externe. Dans votre code, vous venez de mettre i2 à iau moment où vous définissez votre fonction. Cela ne fait iPAS une variable libre. Pourtant, nous considérons votre fonction comme une fermeture, mais sans aucune variable libre, c'est le point.
leemes
2
@leemes, je suis d'accord. Et par rapport à la réponse acceptée, cela ne montre pas vraiment ce qui se passe réellement. :)
Abel
3
Je pense que c'est la meilleure réponse, expliquant les fermetures de manière générale et simple, puis je suis entré dans le cas d'utilisation spécifique. Merci!
tim peterson
22

Je n'ai jamais été satisfait de la façon dont quelqu'un explique cela.

La clé pour comprendre les fermetures est de comprendre à quoi ressemblerait JS sans fermetures.

Sans fermetures, cela générerait une erreur

function outerFunc(){
    var outerVar = 'an outerFunc var';
    return function(){
        alert(outerVar);
    }
}

outerFunc()(); //returns inner function and fires it

Une fois que externalFunc est revenu dans une version imaginaire de JavaScript désactivée par la fermeture, la référence à externalVar serait récupérée et ne laisserait rien là pour que la fonction interne fasse référence.

Les fermetures sont essentiellement les règles spéciales qui interviennent et permettent à ces variables d'exister lorsqu'une fonction interne fait référence à des variables d'une fonction externe. Avec les fermetures, les vars référencés sont conservés même après la fin de la fonction extérieure ou «fermés» si cela vous aide à vous souvenir du point.

Même avec des fermetures, le cycle de vie des vars locaux dans une fonction sans fonction interne qui fait référence à ses locaux fonctionne de la même manière que dans une version sans fermeture. Lorsque la fonction est terminée, les habitants récupèrent les ordures.

Une fois que vous avez une référence dans une fonction interne à une variable externe, c'est comme si un chambranle de porte se mettait en travers de la collecte des ordures pour ces variables référencées.

Une façon peut-être plus précise de regarder les fermetures est que la fonction interne utilise essentiellement la portée interne comme sa propre fondation de portée.

Mais le contexte référencé est en fait, persistant, pas comme un instantané. Le déclenchement répété d'une fonction interne renvoyée qui continue à incrémenter et à enregistrer la variable locale d'une fonction externe continuera à alerter des valeurs plus élevées.

function outerFunc(){
    var incrementMe = 0;
    return function(){ incrementMe++; console.log(incrementMe); }
}
var inc = outerFunc();
inc(); //logs 1
inc(); //logs 2
Erik Reppen
la source
Vous avez raison sur le «cliché» (je pense, vous vous référez à ma réponse) par cela. Je cherchais un mot qui indiquerait le comportement. Dans votre exemple, cela peut être vu comme une construction de fermeture «hotlink». Lors de la capture de la fermeture comme paramètre dans la fonction interne, on pourrait déclarer qu'elle se comporte comme un «instantané». Mais je suis d'accord, des mots mal utilisés ne font qu'ajouter de la confusion au sujet. Si vous avez des suggestions à ce sujet, je mettrai à jour ma réponse.
Andries
Cela pourrait aider à l'explication si vous donnez que la fonction interne était une fonction nommée.
Phillip Senn
Sans fermetures, vous obtiendriez une erreur car vous essayez d'utiliser une variable qui n'existe pas.
Juan Mendes
Hmm ... bon point. Le fait de référencer une variable non définie n'a-t-il jamais généré d'erreur, car il serait finalement recherché comme une propriété sur l'objet global ou suis-je confus avec une affectation à des variables non définies?
Erik Reppen
17

Vous utilisez tous les deux des fermetures.

Je vais avec la définition de Wikipedia ici:

En informatique, une fermeture (également fermeture lexicale ou fermeture de fonction) est une fonction ou une référence à une fonction avec un environnement de référence - un tableau stockant une référence à chacune des variables non locales (également appelées variables libres) de cette fonction . Une fermeture, contrairement à un pointeur de fonction simple, permet à une fonction d'accéder à ces variables non locales même lorsqu'elle est invoquée en dehors de sa portée lexicale immédiate.

La tentative de votre ami utilise clairement la variable i, qui n'est pas locale, en prenant sa valeur et en faisant une copie à stocker dans le local i2.

Votre propre tentative passe i(qui sur le site d'appel est dans la portée) à une fonction anonyme comme argument. Ce n'est pas une fermeture jusqu'à présent, mais cette fonction renvoie une autre fonction qui fait référence à la même chose i2. Puisque l'intérieur de la fonction anonyme intérieure i2n'est pas un local, cela crée une fermeture.

Jon
la source
Oui, mais je pense que le point est de savoir comment il le fait. Il copie juste ià i2, puis définit une certaine logique et exécute cette fonction. Si je n'exécuter immédiatement, mais le stocker dans un var, et exécuté après la boucle, il imprimerait 10, ne serait - il? Il n'a donc pas capturé i.
leemes
6
@leemes: Il a très bien capturé i. Le comportement que vous décrivez n'est pas le résultat d'une fermeture ou d'une non-fermeture; c'est le résultat de la modification de la variable fermée en attendant. Vous faites la même chose en utilisant une syntaxe différente en appelant immédiatement une fonction et en la passant icomme argument (qui copie sa valeur actuelle sur place). Si vous mettez le vôtre setTimeoutdans un autre, setTimeoutla même chose se produira.
Jon
13

Vous et votre ami utilisez tous les deux des fermetures:

Une fermeture est un type spécial d'objet qui combine deux choses: une fonction et l'environnement dans lequel cette fonction a été créée. L'environnement se compose de toutes les variables locales qui étaient dans le champ d'application au moment de la création de la fermeture.

MDN: https://developer.mozilla.org/en-US/docs/JavaScript/Guide/Closures

Dans la fonction de code de votre ami function(){ console.log(i2); }définie à l'intérieur de la fermeture de la fonction anonyme function(){ var i2 = i; ...et peut lire / écrire la variable locale i2.

Dans votre code, la fonction est function(){ console.log(i2); }définie à l'intérieur de la fermeture de la fonction function(i2){ return ...et peut lire / écrire la valeur locale i2(déclarée dans ce cas comme paramètre).

Dans les deux cas, la fonction est function(){ console.log(i2); }ensuite passée setTimeout.

Un autre équivalent (mais avec moins d'utilisation de la mémoire) est:

function fGenerator(i2){
    return function(){
        console.log(i2);
    }
}
for(var i = 0; i < 10; i++) {
    setTimeout(fGenerator(i), 1000);
}
Andrew D.
la source
1
Je ne vois pas pourquoi votre solution par rapport à la solution de mon ami "est plus rapide et utilise moins de mémoire", pourriez-vous élaborer?
brillout
3
Dans votre solution, vous créez 20 objets fonction (2 objets sur chaque boucle: 2x10 = 20). Même résultat en solution de votre frend. Dans "ma" solution, seuls 11 objets fonction sont créés: 1 avant pour la boucle et 10 "à l'intérieur" - 1 + 1x10 = 11. En conséquence - moins d'utilisation de la mémoire et augmentation de la vitesse.
Andrew D.
1
En théorie, ce serait vrai. En pratique, aussi: Voir ce benchmark JSPerf
Rob W
10

Fermeture

Une fermeture n'est pas une fonction ni une expression. Il doit être considéré comme une sorte de «instantané» des variables utilisées en dehors de la fonction et utilisé à l'intérieur de la fonction. Grammaticalement, il faut dire: «prenez la fermeture des variables».

Encore une fois, en d'autres termes: une fermeture est une copie du contexte pertinent des variables dont dépend la fonction.

Encore une fois (naïf): Une fermeture a accès à des variables qui ne sont pas passées en paramètre.

Gardez à l'esprit que ces concepts fonctionnels dépendent fortement du langage / environnement de programmation que vous utilisez. En JavaScript, la fermeture dépend de la portée lexicale (ce qui est vrai dans la plupart des langages c).

Ainsi, le retour d'une fonction revient principalement à une fonction anonyme / sans nom. Lorsque la fonction accède aux variables, non passées en paramètre, et dans sa portée (lexicale), une fermeture a été effectuée.

Donc, concernant vos exemples:

// 1
for(var i = 0; i < 10; i++) {
    setTimeout(function() {
        console.log(i); // closure, only when loop finishes within 1000 ms,
    }, 1000);           // i = 10 for all functions
}
// 2
for(var i = 0; i < 10; i++) {
    (function(){
        var i2 = i; // closure of i (lexical scope: for-loop)
        setTimeout(function(){
            console.log(i2); // closure of i2 (lexical scope:outer function)
        }, 1000)
    })();
}
// 3
for(var i = 0; i < 10; i++) {
    setTimeout((function(i2){
        return function() {
            console.log(i2); // closure of i2 (outer scope)

        }
    })(i), 1000); // param access i (no closure)
}

Tous utilisent des fermetures. Ne confondez pas le point d'exécution avec les fermetures. Si le «cliché» des fermetures est pris au mauvais moment, les valeurs peuvent être inattendues mais certainement une fermeture est prise!

Andries
la source
10

Regardons les deux façons:

(function(){
    var i2 = i;
    setTimeout(function(){
        console.log(i2);
    }, 1000)
})();

Déclare et exécute immédiatement une fonction anonyme qui s'exécute setTimeout()dans son propre contexte. La valeur actuelle de iest préservée en faisant une copie en i2premier; cela fonctionne en raison de l'exécution immédiate.

setTimeout((function(i2){
    return function() {
        console.log(i2);
    }
})(i), 1000);

Déclare un contexte d'exécution pour la fonction interne dans lequel la valeur actuelle de iest conservée dans i2; cette approche utilise également l'exécution immédiate pour conserver la valeur.

Important

Il convient de mentionner que la sémantique d'exécution n'est PAS la même entre les deux approches; votre fonction intérieure est transmise setTimeout()alors que sa fonction intérieure s'appelle setTimeout().

Envelopper les deux codes dans un autre setTimeout()ne prouve pas que seule la deuxième approche utilise des fermetures, il n'y a tout simplement pas la même chose pour commencer.

Conclusion

Les deux méthodes utilisent des fermetures, ce qui revient au goût personnel; la deuxième approche est plus facile à "déplacer" ou à généraliser.

Jack
la source
Je pense que la différence est la suivante: sa solution (1re) est la capture par référence, la mienne (2e) est la capture par valeur. Dans ce cas, cela ne fait aucune différence, mais si je mettais l'exécution dans un autre setTimeout, nous verrions que sa solution a le problème d'utiliser ensuite la valeur finale de i, pas le courant, tandis que le seuil de mine utilise la valeur actuelle (depuis capturée par valeur).
leemes
@leemes Vous capturez tous les deux de la même manière; passer une variable via un argument ou une affectation de fonction est la même chose ... pourriez-vous ajouter à votre question comment vous envelopperiez l'exécution dans une autre setTimeout()?
Ja͢ck
laissez-moi vérifier ceci ... Je voulais montrer que l'objet fonction peut être transmis et que la variable d'origine ipeut être modifiée sans affecter ce que la fonction devrait imprimer, sans dépendre de l'endroit ou du moment où nous l'exécutons.
leemes
Attendez, vous n'avez pas transmis de fonction à (l'extérieur) setTimeout. Supprimez-les (), passant ainsi une fonction, et vous voyez 10 fois la sortie 10.
leemes
@leemes Comme mentionné précédemment, ()c'est exactement ce qui fait fonctionner son code, tout comme votre (i); vous n'avez pas simplement encapsulé son code, vous y avez apporté des modifications ... par conséquent, vous ne pouvez plus faire de comparaison valide.
Ja͢ck
8

J'ai écrit cela il y a quelque temps pour me rappeler ce qu'est une fermeture et comment elle fonctionne dans JS.

Une fermeture est une fonction qui, lorsqu'elle est appelée, utilise la portée dans laquelle elle a été déclarée, et non la portée dans laquelle elle a été appelée. En javaScript, toutes les fonctions se comportent comme ceci. Les valeurs variables dans une étendue persistent tant qu'il existe une fonction qui pointe toujours vers elles. L'exception à la règle est «ceci», qui fait référence à l'objet dans lequel se trouve la fonction lorsqu'elle est appelée.

var z = 1;
function x(){
    var z = 2; 
    y(function(){
      alert(z);
    });
}
function y(f){
    var z = 3;
    f();
}
x(); //alerts '2' 
Nat Darke
la source
6

Après avoir inspecté de près, on dirait que vous utilisez tous les deux la fermeture.

Dans le cas de vos amis, iest accessible dans la fonction anonyme 1 et i2est accessible dans la fonction anonyme 2 où le console.logest présent.

Dans votre cas, vous accédez à l' i2intérieur d'une fonction anonyme où se console.logtrouve. Ajoutez une debugger;déclaration avant console.loget dans les outils de développement Chrome sous "Variables de portée", il indiquera sous quelle portée la variable est.

Ramesh
la source
2
La section "Fermeture" du panneau de droite est utilisée car il n'y a pas de nom plus spécifique. "Local" est une indication plus forte que "Clôture".
Rob W
4

Considérer ce qui suit. Cela crée et recrée une fonction fqui se ferme i, mais différentes!:

i=100;

f=function(i){return function(){return ++i}}(0);
alert([f,f(),f(),f(),f(),f(),f(),f(),f(),f(),f()].join('\n\n'));

f=function(i){return new Function('return ++i')}(0);        /*  function declarations ~= expressions! */
alert([f,f(),f(),f(),f(),f(),f(),f(),f(),f(),f()].join('\n\n'));

tandis que ce qui suit se ferme sur "une" fonction "elle-même"
(eux-mêmes! l'extrait de code suivant utilise un seul référent f)

for(var i = 0; i < 10; i++) {
    setTimeout( new Function('console.log('+i+')'),  1000 );
}

ou pour être plus explicite:

for(var i = 0; i < 10; i++) {
    console.log(    f = new Function( 'console.log('+i+')' )    );
    setTimeout( f,  1000 );
}

NB. la dernière définition de fest function(){ console.log(9) } avant 0 est imprimée.

Caveat! Le concept de fermeture peut être une distraction coercitive de l'essence de la programmation élémentaire:

for(var i = 0; i < 10; i++) {     setTimeout( 'console.log('+i+')',  1000 );      }

x-refs .:
Comment fonctionnent les fermetures JavaScript?
Fermetures Javascript Explication Une fermeture
(JS) nécessite-t-elle une fonction à l'intérieur d'une fonction
Comment comprendre les fermetures en Javascript?
Javascript confusion variable locale et globale

ekim
la source
extraits extraits pour la 1ère fois - Run' only was desired - not sure how to remove the je ne sais pas comment contrôler - Copier`
ekim
-1

Je voudrais partager mon exemple et une explication sur les fermetures. J'ai fait un exemple python et deux figures pour montrer les états de pile.

def maker(a, b, n):
    margin_top = 2
    padding = 4
    def message(msg):
        print('\n * margin_top, a * n, 
            ' ‘ * padding, msg, '  * padding, b * n)
    return message

f = maker('*', '#', 5)
g = maker('', '♥’, 3)

f('hello')
g(‘good bye!')

La sortie de ce code serait la suivante:

*****      hello      #####

      good bye!    ♥♥♥

Voici deux figures pour montrer les piles et la fermeture attachée à l'objet fonction.

lorsque la fonction est renvoyée par le fabricant

lorsque la fonction est appelée plus tard

Lorsque la fonction est appelée via un paramètre ou une variable non locale, le code a besoin de liaisons de variables locales telles que margin_top, padding ainsi que a, b, n. Afin de garantir le fonctionnement du code de fonction, le cadre de pile de la fonction de création qui a disparu depuis longtemps doit être accessible, ce qui est sauvegardé dans la fermeture que nous pouvons trouver avec l'objet de message de fonction.

Eunjung Lee
la source
Je voudrais supprimer cette réponse. Je me suis rendu compte que la question n'est pas de savoir ce qu'est la clôture, alors je voudrais passer à l'autre question.
Eunjung Lee
2
Je pense que vous avez la possibilité de supprimer votre propre contenu. Cliquez sur le deletelien sous la réponse.
Rory McCrossan