web-dev-qa-db-fra.com

Comment structurer le rappel javascript afin que la portée de la fonction soit maintenue correctement

J'utilise XMLHttpRequest et je souhaite accéder à une variable locale dans la fonction de rappel de réussite.

Voici le code:

function getFileContents(filePath, callbackFn) {  
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
        if (xhr.readyState == 4) {
            callbackFn(xhr.responseText);
        }
    }
    xhr.open("GET", chrome.extension.getURL(filePath), true);
    xhr.send();
}

Et je veux l'appeler comme ça:

var test = "lol";

getFileContents("hello.js", function(data) {
    alert(test);
});

Ici, test serait en dehors de la portée de la fonction de rappel, puisque seules les variables de la fonction englobante sont accessibles à l'intérieur de la fonction de rappel. Quel est le meilleur moyen de passer test à la fonction de rappel afin que alert(test); affiche test correctement?

Modifier: 

Maintenant, si j'ai le code suivant appelant la fonction définie ci-dessus:

for (var test in testers) {
    getFileContents("hello.js", function(data) {
        alert(test);
    });
}

Le code alert(test); n’imprime que la dernière valeur de test à partir de la boucle for. Comment puis-je faire en sorte qu'il imprime la valeur de test au moment où la fonction getFileContents a été appelée? (J'aimerais faire ceci sans changer getFileContents car c'est une fonction d'aide très générale et je ne veux pas le rendre spécifique en lui passant une variable spécifique comme test.

32
Chetan

Avec le code que vous avez fourni, test sera toujours inclus dans le rappel. xhr ne sera pas, à part que xhr.responseText soit passé en tant que data.

Mis à jour à partir du commentaire:

En supposant que votre code ressemble à ceci:

for (var test in testers)
  getFileContents("hello"+test+".js", function(data) {
    alert(test);
  });
}

Lors de l'exécution de ce script, test se verra attribuer les valeurs des clés de testers - getFileContents est appelé à chaque fois, ce qui lance une demande en arrière-plan. Lorsque la demande se termine, il appelle le rappel. test va contenir la FINAL VALUE de la boucle, car celle-ci a déjà fini de s'exécuter. 

Il existe une technique appelée fermeture permettant de résoudre ce type de problème. Vous pouvez créer une fonction qui retourne votre fonction de rappel en créant une nouvelle portée que vous pouvez conserver sur vos variables avec:

for (var test in testers) {
  getFileContents("hello"+test+".js", 
    (function(test) { // lets create a function who has a single argument "test"
      // inside this function test will refer to the functions argument
      return function(data) {
        // test still refers to the closure functions argument
        alert(test);
      };
    })(test) // immediately call the closure with the current value of test
  );
}

Cela créera essentiellement une nouvelle portée (avec notre nouvelle fonction) qui "conservera" la valeur de test

Une autre façon d'écrire le même genre de chose:

for (var test in testers) {
  (function(test) { // lets create a function who has a single argument "test"
    // inside this function test will refer to the functions argument
    // not the var test from the loop above
    getFileContents("hello"+test+".js", function(data) {
        // test still refers to the closure functions argument
        alert(test);
    });
  })(test); // immediately call the closure with the value of `test` from `testers`
}
40
gnarf

JavaScript utilise lexical scoping , ce qui signifie que votre deuxième exemple de code fonctionnera exactement comme vous le souhaitez. 

Prenons l'exemple suivant, emprunté au Definitive Guide de David Flanagan1:

var x = "global";

function f() {
  var x = "local";
  function g() { alert(x); }
  g();
}

f();  // Calling this function displays "local"

N'oubliez pas non plus que, contrairement à C, C++ et Java, JavaScript n'a pas d'étendue au niveau des blocs.

En outre, vous pouvez également être intéressé par l'article suivant, que je recommande vivement:


1 David Flanagan: JavaScript - Le guide définitif , quatrième édition, page 48.

7
Daniel Vassallo

Dans ce scénario, le test sera résolu comme prévu, mais la valeur de this peut être différente. Normalement, pour conserver la portée, vous devez en faire un paramètre pour la fonction asynchrone, comme ceci:

function getFileContents(filePath, callbackFn, scope) {  
    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
        if (xhr.readyState == 4) {
            callbackFn.call(scope, xhr.responseText);
        }
    }
    xhr.open("GET", chrome.extension.getURL(filePath), true);
    xhr.send();
}


//then to call it:
var test = "lol";

getFileContents("hello.js", function(data) {
    alert(test);
}, this);
2
Igor Zevaka

J'ai rencontré un problème similaire. Mon code ressemblait à ceci:

for (var i=0; i<textFilesObj.length; i++)
{
    var xmlHttp=new XMLHttpRequest();
    var name = "compile/" + textFilesObj[i].fileName;
    var content = textFilesObj[i].content;

    xmlHttp.onreadystatechange=function()
    {
        if(xmlHttp.readyState==4)
        {
            var responseText = xmlHttp.responseText;
            Debug(responseText);
        }
    }

    xmlHttp.open("POST","save1.php",true);
    xmlHttp.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
    xmlHttp.send("filename="+name+"&text="+encodeURIComponent(content));
}

La sortie était souvent le texte de réponse du dernier objet (mais pas toujours). En fait, c'était un peu aléatoire et même le nombre de réponses variait (pour une entrée constante). Il s'avère que la fonction aurait dû être écrite:

    xmlHttp.onreadystatechange=function()
    {
        if(this.readyState==4)
        {
            var responseText = this.responseText;
            Debug(responseText);
        }
    }

Fondamentalement, dans la première version, l'objet "xmlHttp" est défini sur la version de l'étape en cours de la boucle. La plupart du temps, la boucle aurait été terminée avant l'une des demandes AJAX, aussi, dans ce cas, "xmlHttp" faisait référence à la dernière instance de la boucle. Si cette demande finale se terminait avant les autres demandes, ils imprimeraient tous la réponse de la dernière demande chaque fois que leur état de disponibilité changerait (même si leur état de disponibilité était <4). 

La solution consiste à remplacer "xmlHttp" par "this" afin que la fonction interne fasse référence à l'instance correcte de l'objet de requête chaque fois que le rappel est appelé.

0
Bruce