Reputation: 35
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.
Upvotes: 0
Views: 80
Reputation: 1060
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.
Upvotes: 1
Reputation: 11116
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
Upvotes: 2
Reputation: 8055
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
.
Upvotes: 1