How to structure javascript callback so that function scope is maintained properly
I'm using XMLHttpRequest, and I want to access a local variable in the success callback function.
Here is the 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();
}
And I want to call it like this:
var test = "lol";
getFileContents("hello.js", function(data) {
alert(test);
});
Here, test
would be out of the scope of the callback function, since only the enclosing function's variables are accessible inside the callback function. What is the best way to pass test
to the callback function so the alert(test);
will display test
correctly?
Edit:
Now, if I have the following code calling the function defined above:
for (var test in testers) {
getFileContents("hello.js", function(data) {
alert(test);
});
}
The alert(test);
code only prints the last value of test
from the for
loop. How do I make it so that it prints the开发者_开发技巧 value of test
during the time at which the function getFileContents
was called? (I would like to do this without changing getFileContents
because it's a very general helper function and I don't want to make it specific by passing a specific variable like test
to it.
With the code you have provided test
will still be in scope inside the callback. xhr
will not be, other than xhr.responseText
being passed in as data
.
Updated from comment:
Assuming your code looks something like this:
for (var test in testers)
getFileContents("hello"+test+".js", function(data) {
alert(test);
});
}
As this script runs, test
will be assigned the values of the keys in testers
- getFileContents
is called each time, which starts a request in the background. As the request finishes, it calls the callback. test
is going to contain the FINAL VALUE from the loop, as that loop has already finished executing.
There is a technique you can use called a closure that will fix this sort of problem. You can create a function that returns your callback function, creating a new scope you can hold onto your variables with:
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
);
}
This will basically create a new scope (along with our new function) that will "hold on" to the value of test
.
Another way of writing the same sort of thing:
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`
}
JavaScript uses lexical scoping, which basically means that your second code example will work just like how you intend it to work.
Consider the following example, borrowed from David Flanagan's Definitive Guide1:
var x = "global";
function f() {
var x = "local";
function g() { alert(x); }
g();
}
f(); // Calling this function displays "local"
Also keep in mind that unlike C, C++ and Java, JavaScript does not have block-level scope.
In addition, you may also be interested in checking out the following article, which I highly recommend:
- Mozilla Developer Center: Working with Closures
1 David Flanagan: JavaScript - The Definitive Guide, Fourth Edition, Page 48.
In this scenario, test will be resolved as you'd expect it, but the value of this
might be different. Normally, to preserve the scope, you would make it a parameter to the asynchronous function like so:
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);
I ran into a similar problem. My code looked like this:
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));
}
The output was often the response text of the last object (but not always). In fact it was kind of random and even the number of responses varied (for a constant input). It turns out that the function should have been written:
xmlHttp.onreadystatechange=function()
{
if(this.readyState==4)
{
var responseText = this.responseText;
Debug(responseText);
}
}
Basically, in the first version, the "xmlHttp" object gets set to the version in the current stage of the loop. Most of the time the loop would have finished before any of the AJAX requests finished, so in this case "xmlHttp" referred to the last instance of the loop. If this final request finished before the other requests then they would all print the response from the last request whenever their ready state changed (even if their ready states were < 4).
The solution is to replace "xmlHttp" with "this" so the inner function is referring to the correct instance of the request object every time the callback is called.
精彩评论