Si le rappel est défini dans la même portée, la boucle est définie dans (ce qui est fréquemment le cas), puis le rappel aura accès à la variable d'index. En laissant de côté les détails de NodeJS un instant, considérons cette fonction:
function doSomething(callback) {
callback();
}
Cette fonction accepte une référence de fonction de rappel et tout ce qu'il fait est l'appeler. Pas très excitant. :-)
Maintenant, nous allons utiliser que dans une boucle:
var index;
for (index = 0; index < 3; ++index) {
doSomething(function() {
console.log("index = " + index);
});
}
(Dans le code de calcul intensif — comme un processus serveur — préférable de ne pas faire littéralement ce qui précède dans le code de production, nous » ll y revenir dans un instant)
maintenant, quand nous courons, nous voyons le résultat attendu.
index = 0
index = 1
index = 2
Notre rappel a été en mesure d'accéder à index
, car le rappel est fermeture sur les données dans la portée où il est défini. (Ne vous inquiétez pas au sujet du terme « fermeture » closures are not complicated.)
La raison pour laquelle je l'ai dit est sans doute préférable de ne pas faire littéralement ce qui précède dans le code de production de calcul intensif est que le code crée une fonction sur chaque itération (sauf optimisation de fantaisie dans le compilateur, et V8 est très intelligent, mais l'optimisation de la création de ces fonctions est non-trivial). Voici donc un exemple un peu retravaillé:
var index;
for (index = 0; index < 3; ++index) {
doSomething(doSomethingCallback);
}
function doSomethingCallback() {
console.log("index = " + index);
}
Cela peut paraître un peu surprenant, mais il fonctionne toujours de la même façon, et a toujours la même sortie, parce que doSomethingCallback
est encore une fermeture sur index
, il voit encore la valeur de index
à partir de quand il s'appelle. Mais maintenant, il n'y a qu'une seule fonction doSomethingCallback
, plutôt qu'une nouvelle sur chaque boucle.
Maintenant, nous allons prendre un exemple négatif, quelque chose qui ne le fait pas travail:
foo();
function foo() {
var index;
for (index = 0; index < 3; ++index) {
doSomething(myCallback);
}
}
function myCallback() {
console.log("index = " + index); // <== Error
}
qui échoue, car myCallback
ne définit pas la même portée (ou une portée imbriquée) qui index
est dans défini dans, et donc index
est indéfini dans myCallback
. Enfin, considérons la configuration des gestionnaires d'événements dans une boucle, car il faut faire attention à cela. Ici, nous allons plonger dans NodeJS un peu:
var spawn = require('child_process').spawn;
var commands = [
{cmd: 'ls', args: ['-lh', '/etc' ]},
{cmd: 'ls', args: ['-lh', '/usr' ]},
{cmd: 'ls', args: ['-lh', '/home']}
];
var index, command, child;
for (index = 0; index < commands.length; ++index) {
command = commands[index];
child = spawn(command.cmd, command.args);
child.on('exit', function() {
console.log("Process index " + index + " exited"); // <== WRONG
});
}
Il semble comme ci-dessus devraient travailler de la même façon que nos boucles antérieures ont fait, mais il y a une différence cruciale. Dans nos boucles antérieures, le rappel était appelé immédiatement, et il a donc vu la bonne valeur index
car index
n'avait pas encore eu l'occasion de passer à autre chose. Dans ce qui précède, cependant, nous allons faire tourner la boucle avant que le rappel ne soit appelé. Le résultat? Nous voyons
Process index 3 exited
Process index 3 exited
Process index 3 exited
Ceci est un point crucial. Une fermeture n'a pas de copie des données qu'il ferme, elle a une référence . Ainsi, au moment où le rappel exit
sur chacun de ces processus sera exécuté, la boucle sera déjà terminée, de sorte que tous les trois appels voient la même valeur index
(sa valeur à partir de fin de la boucle).
Nous pouvons résoudre ce problème en ayant la fonction de rappel utiliser une variable différente qui ne changera pas, comme ceci:
var spawn = require('child_process').spawn;
var commands = [
{cmd: 'ls', args: ['-lh', '/etc' ]},
{cmd: 'ls', args: ['-lh', '/usr' ]},
{cmd: 'ls', args: ['-lh', '/home']}
];
var index, command, child;
for (index = 0; index < commands.length; ++index) {
command = commands[index];
child = spawn(command.cmd, command.args);
child.on('exit', makeExitCallback(index));
}
function makeExitCallback(i) {
return function() {
console.log("Process index " + i + " exited");
};
}
Maintenant, nous affichons les valeurs correctes (dans l'ordre la sortie des processus):
Process index 1 exited
Process index 2 exited
Process index 0 exited
la façon dont fonctionne est que le rappel que nous attribuons à l'événement exit
se ferme sur l'argument i
dans l'appel que nous faisons makeExitCallback
. Le premier rappel qui makeExitCallback
crée et retourne se referme sur la valeur i
pour cet appel à makeExitCallback
, le deuxième rappel crée ferme sur la valeur i
pour que appel à makeExitCallback
(qui est différente de la valeur i
pour l'appel précédent), etc.
Si vous donnez the article linked above une lecture, un certain nombre de choses devraient être plus claires. La terminologie de l'article est un peu datée (ECMAScript 5 utilise une terminologie mise à jour), mais les concepts n'ont pas changé.
Je vous recommande de lire [JavaScript Closure Notes] (http://jibbering.com/faq/notes/closures/) - nodes.js joue juste selon les règles. –
@pst: Enh, j'ai vu beaucoup mieux. Par exemple, * "Une fermeture est formée quand l'une de ces fonctions internes est rendue accessible en dehors de la fonction dans laquelle elle était contenue ..." * C'est faux. Une fermeture est créée lorsque vous créez la fonction, arrêt complet. Si la fonction peut être accédée en dehors de sa portée contenant est complètement hors de propos. Il commence aussi à parler de fermetures ayant des effets "potentiellement nocifs", ce qui n'est pas vrai pour le débutant. –