23
votes

I'm reading the Mozilla developer's site on closures, and I noticed in their example for common mistakes, they had this code:

<p id="help">Helpful notes will appear here</p>  
<p>E-mail: <input type="text" id="email" name="email"></p>  
<p>Name: <input type="text" id="name" name="name"></p>  
<p>Age: <input type="text" id="age" name="age"></p>  

and

function showHelp(help) {
  document.getElementById('help').innerHTML = help;
}

function setupHelp() {
  var helpText = [
      {'id': 'email', 'help': 'Your e-mail address'},
      {'id': 'name', 'help': 'Your full name'},
      {'id': 'age', 'help': 'Your age (you must be over 16)'}
    ];

  for (var i = 0; i < helpText.length; i++) {
    var item = helpText[i];
    document.getElementById(item.id).onfocus = function() {
      showHelp(item.help);
    }
  }
}

and they said that for the onFocus event, the code would only show help for the last item because all of the anonymous functions assigned to the onFocus event have a closure around the 'item' variable, which makes sense because in JavaScript variables do not have block scope. The solution was to use 'let item = ...' instead, for then it has block scope.

However, what I wonder is why couldn't you declare 'var item' right above the for loop? Then it has the scope of setupHelp(), and each iteration you are assigning it a different value, which would then be captured as its current value in the closure... right?

5
javascript has let? looking up...Kobi
@Kobi: It's a Mozilla-specific extension to JavaScript. See this.Sasha Chedygov
If it's Mozilla-specific, does that mean I should avoid using it?Nick
Only if you can guarantee that your users will never use a non-Mozilla browser. So, practically, no. Here's a useful reference for feature compatibility: robertnyman.com/javascript/index.htmlEnder

5 Answers

26
votes

Its because at the time item.help is evaluated, the loop would have completed in its entirety. Instead, you can do this with a closure:

for (var i = 0; i < helpText.length; i++) {
   document.getElementById(helpText[i].id).onfocus = function(item) {
           return function() {showHelp(item.help);};
         }(helpText[i]);
}

JavaScript doesn't have block scope but it does have function-scope. By creating a closure, we are capturing the reference to helpText[i] permanently.

20
votes

A closure is a function and the scoped environment of that function.

It helps to understand how Javascript implements scope in this case. It is, in fact, just a series of nested dictionaries. Consider this code:

var global1 = "foo";

function myFunc() {
    var x = 0;
    global1 = "bar";
}

myFunc();

When the program starts running, you have a single scope dictionary, the global dictionary, which might have a number of things defined in it:

{ global1: "foo", myFunc:<function code> }

Say you call myFunc, which has a local variable x. A new scope is created for this function's execution. The function's local scope looks like this:

{ x: 0 }

It also contains a reference to its parent scope. So the entire scope of the function looks like this:

{ x: 0, parentScope: { global1: "foo", myFunc:<function code> } }

This allows myFunc to modify global1. In Javascript, whenever you attempt to assign a value to a variable, it first checks the local scope for the variable name. If it isn't found, it checks the parentScope, and that scope's parentScope, etc. until the variable is found.

A closure is literally a function plus a pointer to that function's scope (which contains a pointer to its parent scope, and so on). So, in your example, after the for loop has finished executing, the scope might look like this:

setupHelpScope = {
  helpText:<...>,
  i: 3, 
  item: {'id': 'age', 'help': 'Your age (you must be over 16)'},
  parentScope: <...>
}

Every closure you create will point to this single scope object. If we were to list every closure that you created, it would look something like this:

[anonymousFunction1, setupHelpScope]
[anonymousFunction2, setupHelpScope]
[anonymousFunction3, setupHelpScope]

When any of these functions executes, it uses the scope object that it was passed - in this case, it's the same scope object for each function! Each one will look at the same item variable and see the same value, which is the last one set by your for loop.

To answer your question, it doesn't matter whether you add var item above the for loop or inside it. Because for loops do not create their own scope, item will be stored in the current function's scope dictionary, which is setupHelpScope. Enclosures generated inside the for loop will always point to setupHelpScope.

Some important notes:

  • This behavior occurs because, in Javascript, for loops do not have their own scope - they just use the enclosing function's scope. This is also true of if, while, switch, etc. If this were C#, on the other hand, a new scope object would be created for each loop, and each closure would contain a pointer to its own unique scope.
  • Notice that if anonymousFunction1 modifies a variable in its scope, it modifies that variable for the other anonymous functions. This can lead to some really bizarre interactions.
  • Scopes are just objects, like the ones you program with. Specifically, they're dictionaries. The JS virtual machine manages their deletion from memory just like anything else - with the garbage collector. For this reason, overuse of closures can create a real memory bloat. Since a closure contains a pointer to a scope object (which in turn contains a pointer to its parent scope object and on and on), the entire scope chain cannot be garbage collected, and has to stick around in memory.

Further reading:

3
votes

I realize the original question is five years old... But you could also just bind a different/special scope to the callback function you assign to each element:

// Function only exists once in memory
function doOnFocus() {
   // ...but you make the assumption that it'll be called with
   //    the right "this" (context)
   var item = helpText[this.index];
   showHelp(item.help);
};

for (var i = 0; i < helpText.length; i++) {
   // Create the special context that the callback function
   // will be called with. This context will have an attr "i"
   // whose value is the current value of "i" in this loop in
   // each iteration
   var context = {index: i};

   document.getElementById(helpText[i].id).onfocus = doOnFocus.bind(context);
}

If you want a one-liner (or close to it):

// Kind of messy...
for (var i = 0; i < helpText.length; i++) {
   document.getElementById(helpText[i].id).onfocus = function(){
      showHelp(helpText[this.index].help);
   }.bind({index: i});
}

Or better yet, you can use EcmaScript 5.1's array.prototype.forEach, which fixes the scope problem for you.

helpText.forEach(function(help){
   document.getElementById(help.id).onfocus = function(){
      showHelp(help);
   };
});
2
votes

New scopes are only created in function blocks (and with, but don't use that). Loops like for don't create new scopes.

So even if you declared the variable outside the loop, you would run into the exact same problem.

1
votes

Even if it's declared outside of the for loop, each of the anonymous functions will still be referring to the same variable, so after the loop, they'll all still point to the final value of item.