0

This link has examples on closures in JavaScript.

In example 5, we have this code:

function buildList(list) {
  var result = [];
  for (var i = 0; i < list.length; i++) {
    var item = 'item' + i;
    result.push( function() {console.log(item + ' ' + list[i])} );
  } 
  return result;
}

function testList() {
  var fnlist = buildList([1,2,3]);
  // Using j only to  help prevent confusion -- could use I.
  for (var j = 0; j < fnlist.length; j++) {
    fnlist[j]();
  }
}

testList(); //logs "item 2 is undefined" 3 times

In the description of the problem, it is stated "Note that when you run the example, "item2 undefined" is alerted three times! This is because just like previous examples, there is only one closure for the local variables for buildList."

What part of the above code is the closure? I'm being thrown off by the example and the explanation because I can't identify what part is being closed, and thus, I cannot figure out why the code results in undefined.

bpoinder85
  • 35
  • 6
  • The closure is the function `function() {console.log(item + ' ' + list[i])}` and the variables `item`, `list`, and `i` that are part of the scope that contains the function instead of part of the function’s scope. The important thing is that, in every function, `item`, `list`, and `i` are always the same set of variables. – Ry- Sep 22 '17 at 17:15

3 Answers3

2

In this example, list, item, and i are being closed on. You are running into issues because the three are not evaluated inside of the function being pushed onto result until the function is actually called.

This is actually a good example of why ES6 added the let keyword as well, because using var there is only 1 i, and 1 item variable in the buildList function that gets closed on, so all functions in the result array point to the same i and same item variables, which, of course get changed in each iteration. It is spitting out undefined because it is trying to use the last value of i that was set, which would be list.length, which is obviously 1 more than the index of the last element in the list, so when it tries to access list[i] it comes back as undefined.

Changing var i = ... to let i = ... in the for loop, and var item = ... to let item = ... fixes this and returns what you would expect.

function buildList(list) {
  var result = [];
  for (let i = 0; i < list.length; i++) {
    let item = 'item' + i;
    // close on i and list. i does not get evaluated until fn gets called,
    // so we need to create a new i for each iteration of the loop, hence the use of let
    result.push( function() {console.log(item + ' ' + list[i])} );
  } 
  return result;
}

function testList() {
  var fnlist = buildList([1,2,3]);
  // Using j only to  help prevent confusion -- could use I.
  for (var j = 0; j < fnlist.length; j++) {
    fnlist[j]();
  }
}

testList();

The way to do this without the use of ES6 let is to create a function that will evaluate and close on i and list for each iteration of the loop manually, like so:

FYI: From a best-practice standpoint, this is actually the proper way to go so that you are not defining functions inside of a loop.

function logFunction (index, list) {
  // inside here, index and list get evaluated as their current values in the loop
  // and then closed on for that specific value in the function being returned
  return function () {
    console.log("item" + index + " " + list[index]);
  }
}
function buildList(list) {
  var result = [];
  for (var i = 0; i < list.length; i++) {
    // pass i and list to function to manually create closure
    result.push( logFunction(i, list) );
  } 
  return result;
}

function testList() {
  var fnlist = buildList([1,2,3]);
  // Using j only to  help prevent confusion -- could use I.
  for (var j = 0; j < fnlist.length; j++) {
    fnlist[j]();
  }
}

testList(); //logs "item 2 is undefined" 3 times
mhodges
  • 10,938
  • 2
  • 28
  • 46
  • I now understand the whole scoping part of the exercise and why your passing the index and list to logFunction(), but if you'll entertain me here, which parts are the closures? In that code (your second snippet), I'm having lines 4-6 as a closure. And it's considered a closure because it returns permanent values that can be used anywhere, thus closing out their ability to change no matter the level of scope. Is that correct? – bpoinder85 Sep 22 '17 at 18:17
  • @bpoinder85 Not exactly. Closures refer to context, not mutability of a value. When you create a closure, you **enclose** a variable into a given scope, even though it is lexically out of scope at execution time. – mhodges Sep 22 '17 at 19:37
  • @bpoinder85 What happens in the `logFunction` is that the function being returned knows it needs a variable that is outside of its lexical scope. When you execute that function (without closures), it will know nothing about `index` or `list`, because they are variables local to the `logFunction`. When `logFunction` returns, those local variables go away. However, since they are needed by the anonymous function being returned, it will retain those variables, which is the definition of a closure - variables living outside of the context in which they are lexically defined. – mhodges Sep 22 '17 at 19:42
1

The closure is:

function() {console.log(item + ' ' + list[i])}

When the array is built, the i passed is a reference to the i variable outside of the closure function scope. The i variable is always the same and is incrementing (because of the for loop) so, when you call each closure function it tries to access to list[list.length] (because i becomes 2 inside the loop which is from 0 to list.length-1) therefore you got an undefined exception.

Ernesto Schiavo
  • 1,020
  • 2
  • 12
  • 33
  • Can you please clarify what you mean by I is always the same and is incrementing? This is at odds with each other. If I becomes 2, why is the final result not just "Item 2 3" each three times? – bpoinder85 Sep 22 '17 at 20:14
1

This would be the closure: function() {console.log(item + ' ' + list[i])} because it's using i and other variables from the parent scope.

What happens when you run the code ?

Step: 1. push first function, which has access to parent scope - i === 0

Step: 2. push second function - same thing -> i === 1 ( this is the same i as for step 1. - so now both functions have i === 1

...

Step: (list.length-1). push list.length - 1 function in the array -> now i === list.length-1 (but this is the same i to which all closures have access to.

One last step is i++ from the for.

Now you start calling the functions you created but they all have reference to the same i which now is equal to list.length and when it accesses list[list.length] it's outside the bounds of the array so it's undefined.

sirrocco
  • 7,975
  • 4
  • 59
  • 81