ChatGPT解决这个技术问题 Extra ChatGPT

JavaScript closure inside loops – simple practical example

var funcs = []; // let's create 3 functions for (var i = 0; i < 3; i++) { // and store them in funcs funcs[i] = function() { // each should log its value. console.log("My value: " + i); }; } for (var j = 0; j < 3; j++) { // and now let's run each one to see funcs[j](); }

It outputs this:

My value: 3 My value: 3 My value: 3

Whereas I'd like it to output:

My value: 0 My value: 1 My value: 2

The same problem occurs when the delay in running the function is caused by using event listeners:

var buttons = document.getElementsByTagName("button"); // let's create 3 functions for (var i = 0; i < buttons.length; i++) { // as event listeners buttons[i].addEventListener("click", function() { // each should log its value. console.log("My value: " + i); }); }

… or asynchronous code, e.g. using Promises:

// Some async wait function const wait = (ms) => new Promise((resolve, reject) => setTimeout(resolve, ms)); for (var i = 0; i < 3; i++) { // Log `i` as soon as each promise resolves. wait(i * 100).then(() => console.log(i)); }

It is also apparent in for in and for of loops:

const arr = [1,2,3]; const fns = []; for(var i in arr){ fns.push(() => console.log(`index: ${i}`)); } for(var v of arr){ fns.push(() => console.log(`value: ${v}`)); } for(var f of fns){ f(); }

What’s the solution to this basic problem?

You sure you don't want funcs to be an array, if you're using numeric indices? Just a heads up.
This is really confusing problem. This article help me in understanding it. Might it help others too.
Another simple and explaned solution: 1) Nested Functions have access to the scope "above" them; 2) a closure solution... "A closure is a function having access to the parent scope, even after the parent function has closed".
In ES6, a trivial solution is to declare the variable i with let, which is scoped to the body of the loop.
This is why I hate javascript.

3
3limin4t0r

Well, the problem is that the variable i, within each of your anonymous functions, is bound to the same variable outside of the function.

ES6 solution: let

ECMAScript 6 (ES6) introduces new let and const keywords that are scoped differently than var-based variables. For example, in a loop with a let-based index, each iteration through the loop will have a new variable i with loop scope, so your code would work as you expect. There are many resources, but I'd recommend 2ality's block-scoping post as a great source of information.

for (let i = 0; i < 3; i++) {
  funcs[i] = function() {
    console.log("My value: " + i);
  };
}

Beware, though, that IE9-IE11 and Edge prior to Edge 14 support let but get the above wrong (they don't create a new i each time, so all the functions above would log 3 like they would if we used var). Edge 14 finally gets it right.

ES5.1 solution: forEach

With the relatively widespread availability of the Array.prototype.forEach function (in 2015), it's worth noting that in those situations involving iteration primarily over an array of values, .forEach() provides a clean, natural way to get a distinct closure for every iteration. That is, assuming you've got some sort of array containing values (DOM references, objects, whatever), and the problem arises of setting up callbacks specific to each element, you can do this:

var someArray = [ /* whatever */ ];
// ...
someArray.forEach(function(arrayElement) {
  // ... code code code for this one element
  someAsynchronousFunction(arrayElement, function() {
    arrayElement.doSomething();
  });
});

The idea is that each invocation of the callback function used with the .forEach loop will be its own closure. The parameter passed in to that handler is the array element specific to that particular step of the iteration. If it's used in an asynchronous callback, it won't collide with any of the other callbacks established at other steps of the iteration.

If you happen to be working in jQuery, the $.each() function gives you a similar capability.

Classic solution: Closures

What you want to do is bind the variable within each function to a separate, unchanging value outside of the function:

var funcs = []; function createfunc(i) { return function() { console.log("My value: " + i); }; } for (var i = 0; i < 3; i++) { funcs[i] = createfunc(i); } for (var j = 0; j < 3; j++) { // and now let's run each one to see funcs[j](); }

Since there is no block scope in JavaScript - only function scope - by wrapping the function creation in a new function, you ensure that the value of "i" remains as you intended.


isn't function createfunc(i) { return function() { console.log("My value: " + i); }; } still closure because it uses the variable i?
Unfortunately, this answer is outdated and nobody will see the correct answer at the bottom - using Function.bind() is definitely preferable by now, see stackoverflow.com/a/19323214/785541.
@Wladimir: Your suggestion that .bind() is "the correct answer" isn't right. They each have their own place. With .bind() you can't bind arguments without binding the this value. Also you get a copy of the i argument without the ability to mutate it between calls, which sometimes is needed. So they're quite different constructs, not to mention that .bind() implementations have been historically slow. Sure in the simple example either would work, but closures are an important concept to understand, and that's what the question was about.
Please stop using these for-return function hacks, use [].forEach or [].map instead because they avoid reusing the same scope variables.
@ChristianLandgren: That's only useful if you're iterating an Array. These techniques aren't "hacks". They're essential knowledge.
h
henser

Try:

var funcs = []; for (var i = 0; i < 3; i++) { funcs[i] = (function(index) { return function() { console.log("My value: " + index); }; }(i)); } for (var j = 0; j < 3; j++) { funcs[j](); }

Edit (2014):

Personally I think @Aust's more recent answer about using .bind is the best way to do this kind of thing now. There's also lo-dash/underscore's _.partial when you don't need or want to mess with bind's thisArg.


any explanation about the }(i)); ?
@aswzen I think it passes i as the argument index to the function.
it is actually creating local variable index.
Immediately Invoke Function Expression, aka IIFE. (i) is the argument to the anonymous function expression that is invoked immediately and index becomes set from i.
H
Håken Lid

Another way that hasn't been mentioned yet is the use of Function.prototype.bind

var funcs = {}; for (var i = 0; i < 3; i++) { funcs[i] = function(x) { console.log('My value: ' + x); }.bind(this, i); } for (var j = 0; j < 3; j++) { funcs[j](); }

UPDATE

As pointed out by @squint and @mekdev, you get better performance by creating the function outside the loop first and then binding the results within the loop.

function log(x) { console.log('My value: ' + x); } var funcs = []; for (var i = 0; i < 3; i++) { funcs[i] = log.bind(this, i); } for (var j = 0; j < 3; j++) { funcs[j](); }


This is what I do these days too, I also like lo-dash/underscore's _.partial
.bind() will be largely obsolete with ECMAScript 6 features. Besides, this actually creates two functions per iteration. First the anonymous, then the one generated by .bind(). Better use would be to create it outside the loop, then .bind() it inside.
@squint @mekdev - You both are correct. My initial example was written quickly to demonstrate how bind is used. I've added another example per your suggestions.
I think instead of wasting computation over two O(n) loops, just do for (var i = 0; i < 3; i++) { log.call(this, i); }
.bind() does what the accepted answer suggests PLUS fiddles with this.
h
henser

Using an Immediately-Invoked Function Expression, the simplest and most readable way to enclose an index variable:

for (var i = 0; i < 3; i++) { (function(index) { console.log('iterator: ' + index); //now you can also loop an ajax call here //without losing track of the iterator value: $.ajax({}); })(i); }

This sends the iterator i into the anonymous function of which we define as index. This creates a closure, where the variable i gets saved for later use in any asynchronous functionality within the IIFE.


For further code readability and to avoid confusion as to which i is what, I'd rename the function parameter to index.
How would you use this technique to define the array funcs described in the original question?
@Nico The same way as shown in the original question, except you would use index instead of i.
@JLRishe var funcs = {}; for (var i = 0; i < 3; i++) { funcs[i] = (function(index) { return function() {console.log('iterator: ' + index);}; })(i); }; for (var j = 0; j < 3; j++) { funcs[j](); }
@Nico In OP's particular case, they're just iterating over numbers, so this wouldn't be a great case for .forEach(), but a lot of the time, when one is starting off with an array, forEach() is a good choice, like: var nums [4, 6, 7]; var funcs = {}; nums.forEach(function (num, i) { funcs[i] = function () { console.log(num); }; });
C
Community

Bit late to the party, but I was exploring this issue today and noticed that many of the answers don't completely address how Javascript treats scopes, which is essentially what this boils down to.

So as many others mentioned, the problem is that the inner function is referencing the same i variable. So why don't we just create a new local variable each iteration, and have the inner function reference that instead?

//overwrite console.log() so you can see the console output console.log = function(msg) {document.body.innerHTML += '

' + msg + '

';}; var funcs = {}; for (var i = 0; i < 3; i++) { var ilocal = i; //create a new local variable funcs[i] = function() { console.log("My value: " + ilocal); //each should reference its own local variable }; } for (var j = 0; j < 3; j++) { funcs[j](); }

Just like before, where each inner function outputted the last value assigned to i, now each inner function just outputs the last value assigned to ilocal. But shouldn't each iteration have it's own ilocal?

Turns out, that's the issue. Each iteration is sharing the same scope, so every iteration after the first is just overwriting ilocal. From MDN:

Important: JavaScript does not have block scope. Variables introduced with a block are scoped to the containing function or script, and the effects of setting them persist beyond the block itself. In other words, block statements do not introduce a scope. Although "standalone" blocks are valid syntax, you do not want to use standalone blocks in JavaScript, because they don't do what you think they do, if you think they do anything like such blocks in C or Java.

Reiterated for emphasis:

JavaScript does not have block scope. Variables introduced with a block are scoped to the containing function or script

We can see this by checking ilocal before we declare it in each iteration:

//overwrite console.log() so you can see the console output console.log = function(msg) {document.body.innerHTML += '

' + msg + '

';}; var funcs = {}; for (var i = 0; i < 3; i++) { console.log(ilocal); var ilocal = i; }

This is exactly why this bug is so tricky. Even though you are redeclaring a variable, Javascript won't throw an error, and JSLint won't even throw a warning. This is also why the best way to solve this is to take advantage of closures, which is essentially the idea that in Javascript, inner functions have access to outer variables because inner scopes "enclose" outer scopes.

https://i.stack.imgur.com/60fH9.png

This also means that inner functions "hold onto" outer variables and keep them alive, even if the outer function returns. To utilize this, we create and call a wrapper function purely to make a new scope, declare ilocal in the new scope, and return an inner function that uses ilocal (more explanation below):

//overwrite console.log() so you can see the console output console.log = function(msg) {document.body.innerHTML += '

' + msg + '

';}; var funcs = {}; for (var i = 0; i < 3; i++) { funcs[i] = (function() { //create a new scope using a wrapper function var ilocal = i; //capture i into a local var return function() { //return the inner function console.log("My value: " + ilocal); }; })(); //remember to run the wrapper function } for (var j = 0; j < 3; j++) { funcs[j](); }

Creating the inner function inside a wrapper function gives the inner function a private environment that only it can access, a "closure". Thus, every time we call the wrapper function we create a new inner function with it's own separate environment, ensuring that the ilocal variables don't collide and overwrite each other. A few minor optimizations gives the final answer that many other SO users gave:

//overwrite console.log() so you can see the console output console.log = function(msg) {document.body.innerHTML += '

' + msg + '

';}; var funcs = {}; for (var i = 0; i < 3; i++) { funcs[i] = wrapper(i); } for (var j = 0; j < 3; j++) { funcs[j](); } //creates a separate environment for the inner function function wrapper(ilocal) { return function() { //return the inner function console.log("My value: " + ilocal); }; }

Update

With ES6 now mainstream, we can now use the new let keyword to create block-scoped variables:

//overwrite console.log() so you can see the console output console.log = function(msg) {document.body.innerHTML += '

' + msg + '

';}; var funcs = {}; for (let i = 0; i < 3; i++) { // use "let" to declare "i" funcs[i] = function() { console.log("My value: " + i); //each should reference its own local variable }; } for (var j = 0; j < 3; j++) { // we can use "var" here without issue funcs[j](); }

Look how easy it is now! For more information see this answer, which my info is based off of.


There is now such a thing as block scoping in JavaScript using the let and const keywords. If this answer were to expand to include that, it would be much more globally useful in my opinion.
@TinyGiant sure thing, I added some info about let and linked a more complete explanation
@woojoo666 Could your answer also work for calling two alternating URL's in a loop like so: i=0; while(i < 100) { setTimeout(function(){ window.open("https://www.bbc.com","_self") }, 3000); setTimeout(function(){ window.open("https://www.cnn.com","_self") }, 3000); i++ }? (could replace window.open() with getelementbyid......)
@nuttyaboutnatty sorry about such a late reply. It doesn't seem like the code in your example already works. You aren't using i in your timeout functions, so you don't need a closure
whoops, sorry, meant to say "it seems like the code in your example already works"
h
henser

With ES6 now widely supported, the best answer to this question has changed. ES6 provides the let and const keywords for this exact circumstance. Instead of messing around with closures, we can just use let to set a loop scope variable like this:

var funcs = []; for (let i = 0; i < 3; i++) { funcs[i] = function() { console.log("My value: " + i); }; }

val will then point to an object that is specific to that particular turn of the loop, and will return the correct value without the additional closure notation. This obviously significantly simplifies this problem.

const is similar to let with the additional restriction that the variable name can't be rebound to a new reference after initial assignment.

Browser support is now here for those targeting the latest versions of browsers. const/let are currently supported in the latest Firefox, Safari, Edge and Chrome. It also is supported in Node, and you can use it anywhere by taking advantage of build tools like Babel. You can see a working example here: http://jsfiddle.net/ben336/rbU4t/2/

Docs here:

const

let

Beware, though, that IE9-IE11 and Edge prior to Edge 14 support let but get the above wrong (they don't create a new i each time, so all the functions above would log 3 like they would if we used var). Edge 14 finally gets it right.


Unfortunately, 'let' is still not fully supported, especially in mobile. developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/…
As of June '16, let is supported in all major browser versions except iOS Safari, Opera Mini and Safari 9. Evergreen browsers support it. Babel will transpile it correctly to keep the expected behaviour without high compliancy mode switched on.
@DanPantry yeah about time for an update :) Updated to better reflect the current state of things, including adding a mention of const, doc links and better compatibility info.
Isn't this why we use babel to transpile our code so browsers that don't support ES6/7 can understand what's going on?
N
Nae

Another way of saying it is that the i in your function is bound at the time of executing the function, not the time of creating the function.

When you create the closure, i is a reference to the variable defined in the outside scope, not a copy of it as it was when you created the closure. It will be evaluated at the time of execution.

Most of the other answers provide ways to work around by creating another variable that won't change the value for you.

Just thought I'd add an explanation for clarity. For a solution, personally, I'd go with Harto's since it is the most self-explanatory way of doing it from the answers here. Any of the code posted will work, but I'd opt for a closure factory over having to write a pile of comments to explain why I'm declaring a new variable(Freddy and 1800's) or have weird embedded closure syntax(apphacker).


T
Thatkookooguy

What you need to understand is the scope of the variables in javascript is based on the function. This is an important difference than say c# where you have block scope, and just copying the variable to one inside the for will work.

Wrapping it in a function that evaluates returning the function like apphacker's answer will do the trick, as the variable now has the function scope.

There is also a let keyword instead of var, that would allow using the block scope rule. In that case defining a variable inside the for would do the trick. That said, the let keyword isn't a practical solution because of compatibility.

var funcs = {}; for (var i = 0; i < 3; i++) { let index = i; //add this funcs[i] = function() { console.log("My value: " + index); //change to the copy }; } for (var j = 0; j < 3; j++) { funcs[j](); }


@nickf which browser? as I said, it has compatibility issues, with that I mean serious compatibility issues, like I don't think let is supported in IE.
@nickf yes, check this reference: developer.mozilla.org/En/New_in_JavaScript_1.7 ... check the let definitions section, there is an onclick example inside a loop
@nickf hmm, actually you have to explicitly specify the version: