Question Fermetures JavaScript et fonctions anonymes


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

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

La mauvaise solution sans fermetures ou quelque chose près de fermetures serait:

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

qui va bien sûr imprimer 10 fois la valeur de i après la boucle, à savoir 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'utilise pas fermeture capturer imais il insiste sur le fait qu'il l'est. J'ai prouvé qu'il n'utilise pas fermetures en mettant le corps de la boucle pour dans un autre setTimeout (passant sa fonction anonyme à setTimeout), impression 10 fois 10 fois de plus. La même chose s'applique si je stocke sa fonction dans un var et l'exécuter après la boucle, imprimant également 10 fois 10. Donc, mon argument est que il n'a pas vraiment Capturer la valeur de i, en faisant sa version ne pas une fermeture.

Ma tentative était la suivante:

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

Donc je capture i (nommé i2 dans la fermeture), mais maintenant je revenir une autre fonction et passer cela autour. Dans mon cas, la fonction passée à setTimeout capture vraiment i.

Maintenant, qui utilise des fermetures et qui ne l'est pas?

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


523
2017-10-17 08:36


origine


Réponses:


Note de l'éditeur: Toutes les fonctions en JavaScript sont des fermetures comme expliqué dans ce poster. Cependant, nous ne souhaitons identifier qu’un sous-ensemble de ces fonctions intéressant d'un point de vue théorique. Désormais toute référence au mot fermeture se référera à ce sous-ensemble de fonctions, sauf indication contraire.

Une explication simple pour les fermetures:

  1. Prenez une fonction. Appelons-le F.
  2. Liste 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 parentale de F) alors:
    1. Il doit y avoir seulement une portée parentale de F à laquelle une variable libre est liée.
    2. Si F est en référence de dehors cette portée parentale, alors il devient une fermeture pour cette variable libre.
    3. Cette variable libre est appelée une valeur élevée de la fermeture F.

Utilisons maintenant ceci pour déterminer qui utilise les fermetures et qui ne les utilise 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: f et g. Voyons voir si ce sont des fermetures:

Pour f:

  1. Liste les variables:
    1. i2 est un local variable.
    2. i est un gratuit variable.
    3. setTimeout est un gratuit variable.
    4. g est un local variable.
    5. console est un gratuit variable.
  2. Recherchez la portée parent à laquelle chaque variable libre est liée:
    1. i est lié à la portée mondiale.
    2. setTimeout est lié à la portée mondiale.
    3. console est lié à la portée mondiale.
  3. Dans quelle portée est la fonction en référence? le portée mondiale.
    1. Par conséquent i n'est pas fermé sur par f.
    2. Par conséquent setTimeout n'est pas fermé sur par f.
    3. Par conséquent console n'est pas fermé sur par f.

Ainsi, la fonction f Ce n'est pas une fermeture.

Pour g:

  1. Liste les variables:
    1. console est un gratuit variable.
    2. i2 est un gratuit variable.
  2. Recherchez la portée parent à laquelle chaque variable libre est liée:
    1. console est lié à la portée mondiale.
    2. i2 est lié à la portée de f.
  3. Dans quelle portée est la fonction en référence? le champ d'application setTimeout.
    1. Par conséquent console n'est pas fermé sur par g.
    2. Par conséquent i2 est fermé sur par g.

Ainsi, la fonction g est une fermeture pour la variable libre i2 (qui est une valeur pour g) quand ses en référence de l'Intérieur setTimeout.

Mauvais pour vous: Votre ami utilise une fermeture. La fonction interne 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: f et g. Voyons voir si ce sont des fermetures:

Pour f:

  1. Liste les variables:
    1. i2 est un local variable.
    2. g est un local variable.
    3. console est un gratuit variable.
  2. Recherchez la portée parent à laquelle chaque variable libre est liée:
    1. console est lié à la portée mondiale.
  3. Dans quelle portée est la fonction en référence? le portée mondiale.
    1. Par conséquent console n'est pas fermé sur par f.

Ainsi, la fonction f Ce n'est pas une fermeture.

Pour g:

  1. Liste les variables:
    1. console est un gratuit variable.
    2. i2 est un gratuit variable.
  2. Recherchez la portée parent à laquelle chaque variable libre est liée:
    1. console est lié à la portée mondiale.
    2. i2 est lié à la portée de f.
  3. Dans quelle portée est la fonction en référence? le champ d'application setTimeout.
    1. Par conséquent console n'est pas fermé sur par g.
    2. Par conséquent i2 est fermé sur par g.

Ainsi, la fonction g est une fermeture pour la variable libre i2 (qui est une valeur pour g) quand ses en référence de l'Intérieur setTimeout.

Bien pour vous: Vous utilisez une fermeture. La fonction interne est une fermeture.

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

Modifier: Une explication simple pour savoir pourquoi 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 lexicalScope et regularFunction ne sont pas des fermetures de la définition ci-dessus.
  2. Lorsque nous exécutons le programme nous attendons  message être alerté car  regularFunction n'est pas une fermeture (c'est-à-dire qu'elle a accès à tout les variables dans sa portée parent - y compris message).
  3. Lorsque nous exécutons le programme nous observons cette message est en effet alerté.

Ensuite, considérons le programme suivant (c'est le 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 seulement closureFunction est une fermeture de la définition ci-dessus.
  2. Lorsque nous exécutons le programme nous attendons  message ne pas être alerté car  closureFunction est une fermeture (c'est-à-dire qu'elle n'a accès qu'à tous ses variables non-locales à l'heure à laquelle la fonction est créée (voir cette réponse) - cela n'inclut pas message).
  3. Lorsque nous exécutons le programme nous observons cette message est en train d'être alerté.

Qu'en déduisons-nous de cela?

  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 ses chaîne de portée avec. Les fermetures n'ont pas de séparé environnement de référencement.
  3. Une fermeture est juste comme toutes les autres fonctions. Nous appelons simplement les fermetures quand ils sont en référence dans une portée à l'extérieur le champ d'application auquel ils appartiennent car c'est un cas intéressant.

617
2017-10-17 10:01



Selon le closure définition:

Une "fermeture" est une expression (typiquement une fonction) qui peut avoir variables libres avec un environnement qui lie ces variables (qui "ferme" l'expression).

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


91
2017-10-17 08:59



En un mot Fermetures Javascript autoriser une fonction à accéder à une variable C'est déclaré dans une fonction lexicale-parent.

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

Scopes

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

Considérons 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 des copies

hello
hello
2
Am I Accessible?

Considérons maintenant le cas où nous avons une fonction g défini 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 f la parent lexical de g. Comme expliqué précédemment, nous avons maintenant 2 portées; Le viseur f et la portée g.

Mais une portée est "dans" l'autre portée, ainsi la portée de la fonction enfant fait-elle partie de la portée de la fonction parente? Que se passe-t-il avec les variables déclarées dans la portée de la fonction parente? serai-je en mesure d'y accéder à partir de la portée de la fonction enfant? C'est exactement là où les fermetures interviennent.

Fermetures

En JavaScript, la fonction g ne peut pas accéder uniquement aux variables déclarées dans la portée g mais aussi accéder à toutes les variables déclarées dans la portée de la fonction parente f.

Considérez 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 des copies

hello
undefined

Regardons la ligne console.log(foo);. À ce stade, nous sommes dans la portée g et nous essayons d'accéder à la variable foo qui est déclaré dans la portée f. Mais comme indiqué précédemment, nous pouvons accéder à toute variable déclarée dans une fonction parentale lexicale, ce qui est le cas ici; g est le parent lexical de f. Donc hello est imprimé.
Regardons maintenant la ligne console.log(bar);. À ce stade, nous sommes dans la portée f et nous essayons d'accéder à la variable bar qui est déclaré dans la portée g. bar n'est pas déclaré dans la portée actuelle et la fonction g n'est pas le parent de f, donc bar est indéfini

En fait, nous pouvons également accéder aux variables déclarées dans le cadre d'une fonction lexicale «grand parent». Par conséquent, s'il y aurait une fonction h défini dans la fonction g

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

puis h serait en mesure d'accéder à toutes les variables déclarées dans la portée de la fonction h, g, et f. Ceci est fait avec fermetures. En JavaScript fermetures permet d'accéder à toute variable déclarée dans la fonction parentale lexicale, dans la fonction grand parent lexicale, dans la fonction parentale grand-grand-grand, etc. Cela peut être vu comme 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 parente 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. Toute variable non déclarée dans une fonction est considérée comme déclarée dans la portée globale. La portée globale 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 du windowobjet.

Il y a donc exactement deux manières de déclarer une variable foo dans le périmètre global; soit en ne le déclarant pas dans une fonction ou en définissant la propriété foo de l'objet fenêtre.

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-No-Closure. 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

D'accord, 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)
  })();
}

donc cela va imprimer undefined 10 fois dans JavaScript-No-Closure.

D'où 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);
}

donc cela va imprimer undefined 10 fois dans JavaScript-No-Closure.

Les deux solutions utilisent des fermetures.

Edit: il est supposé que ces 3 extraits de code ne sont pas définis dans la portée globale. Sinon, les variables foo et i serait lié à la window objet et donc accessible à travers le window objet à la fois JavaScript et JavaScript-No-Closure.


46
2017-10-18 01:51



Je n'ai jamais été heureux de la façon dont tout le monde explique cela.

La clé pour comprendre les fermetures est de comprendre ce que serait JS sans les fermetures.

Sans les fermetures, cela jetterait 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 retourné dans une version imaginaire désactivée de JavaScript, la référence à outerVar serait récupérée et ne laisserait rien là pour que la fonction interne puisse être référencée.

Les fermetures sont essentiellement les règles spéciales qui entrent en jeu et permettent à ces vars d’exister quand une fonction interne fait référence aux variables d’une fonction externe. Avec les fermetures, les vars référencés sont maintenus même après que la fonction externe soit terminée ou «fermée» 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 reçoivent les ordures ménagères.

Une fois que vous avez une référence dans une fonction interne à une variable externe, c'est comme si un jambage de porte était mis en travers de la récupération de place pour ces variables référencées.

Une manière peut-être plus précise d’examiner les fermetures est que la fonction interne utilise fondamentalement la portée interne en tant que propre portée de la portée.

Mais le contexte référencé est en fait, persistant, pas comme un instantané. Tirer à plusieurs reprises une fonction interne renvoyée qui continue à s'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

19
2017-10-17 09:08



Vous utilisez tous les deux des fermetures.

Je vais avec le Définition Wikipedia ici:

En informatique, une fermeture (également fermeture lexicale ou fonction   fermeture) est une fonction ou une référence à une fonction associée à   référençant environnement-une table stockant une référence à chacun des   variables non locales (également appelées variables libres) de cette fonction.   Une fermeture, contrairement à un simple pointeur, permet à une fonction d'accéder   ces variables non-locales même lorsqu'elles sont invoquées en dehors de son immédiat   portée lexicale.

La tentative de votre ami utilise clairement la variable i, qui est non local, en prenant sa valeur et en faisant une copie pour stocker dans le local i2.

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


16
2017-10-17 09:03



Vous et votre ami utilisez les 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 est constitué de toutes les variables locales qui étaient dans le champ au moment où la fermeture a été créée.

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

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

Dans votre fonction de code function(){ console.log(i2); } défini à l'intérieur de la fermeture de la fonction function(i2){ return ... et peut lire / écrire local précieux i2 (déclaré dans ce cas en tant que paramètre).

Dans les deux cas, la fonction function(){ console.log(i2); } puis passé dans 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);
}

12
2017-10-17 09:49



Fermeture

Une fermeture n'est pas une fonction, et pas une expression. Cela doit être vu comme une sorte de «snapshot» des variables utilisées en dehors de la fonctioncope et utilisé dans la fonction. Sur le plan grammatical, on devrait 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.

Une fois de plus (naïf): Une fermeture a accès aux 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, renvoyer une fonction renvoie la plupart du temps 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!


9
2017-10-17 09:20



Regardons les deux manières:

(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 i est conservé en faisant une copie dans i2 premier; cela fonctionne à cause 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 selon laquelle la valeur actuelle de i est conservé dans i2; Cette approche utilise également une exécution immédiate pour préserver 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 interne est passée à setTimeout()alors que sa fonction interne appelle setTimeout() lui-même.

Envelopper les deux codes dans un autre setTimeout() ne prouve pas que seule la deuxième approche utilise des fermetures, ce n’est tout simplement pas la même chose au départ.

Conclusion

Les deux méthodes utilisent des fermetures, de sorte qu'il s'agit d'un goût personnel; la deuxième approche est plus facile à «déplacer» ou à généraliser.


9
2017-12-21 19:10