Reputation: 13
I am not sure if it's related to closure or it's related to JS call by assigning and hope someone could help clarify this.
The expected result below is to print A, B, C separately.
I guess the reason it's not working is that when the function added to array is executed, the name variable inside the function points to value C so the result is C,C,C not A,B,C
Then I tried to use IIFE to encapsulate(not sure if it's the right word) name variable and it works.
But what confuses me is that I thought the primitive variable should have passed value instead of reference? Or it is not the cause for this outcome and related to something else (maybe mysterious closure)?
This is not working
let names = ["A", "B", "C"];
let targetPages = [];
function setName() {
for (var name of names) {
targetPages.push(function() {
console.log(name);
});
};
};
setName();
for (var f in targetPages) {
targetPages[f]();
}
This is working.
let names = ["A", "B", "C"];
let targetPages = [];
function setTarget() {
for(var name of names) {
(function(n) {
targetPages.push(function() {
console.log(n);
});
}(name));
};
};
setTarget();
for(var f in targetPages) {
targetPages[f]();
}
Upvotes: 1
Views: 275
Reputation: 30098
According to mdn:
The var statement declares a function-scoped or globally-scoped variable, optionally initializing it to a value.
So loop and if
conditional statements with their block scopes are not respected by var
. Hence, making the first of your example display the last value of the variable name
.
You can take the example below as a way to clarify how var
statements work:
for(var i = 0; i < 10; i++) {
var j = 100;
}
console.log(i, j);
Since variables declared by var
only respects function-scope and global-scope, then declarations within a block-scope is not respected as an isolated variable.
Your second example uses an IIFE, which is technically calling the function immediately with the correct name
argument. This coincides with how mdn explains how var
statements work.
An alternative to using an IIFE
is the usage of const and let, which respects block-scope.
I'll use your first example, and use const
instead of var
:
let names = ["A", "B", "C"];
let targetPages = [];
function setName() {
for (const name of names) {
targetPages.push(function() {
console.log(name);
});
};
};
setName();
for (const f in targetPages) {
targetPages[f]();
}
Conclusion:
Using const
and let
let's you avoid these problems in development. But if you're supporting older browser, e.g. IE11, which does not support these statements, then you may use babel to transpile or compile your code to work with such older browsers.
Upvotes: 1
Reputation: 1448
The thing that is causing this is variable hoisting. In JS, defining a variable with var
makes that variable function-scope, and the declaration of that variable is hoisted to the top of the block.
Let's take your first code example of the function setName
. Due to variable hoisting, your code is identical to this:
function setName() {
var name; // hoisted declaration
for (let i = 0; i < names.length; i++) {
name = names[i]; // initialization
targetPages.push(function() {
console.log(name);
});
};
// `name` has a final value of "C" after the for loop
};
What this means is that when you call setName()
, targetPages
gets three functions that all reference the name
variable defined in setName
. However, note that at the end of the for loop inside setName
, name
is set to "C"
. Therefore, if you try to call any of the targetPages[f]
functions, then you will print out the new value of name
- since it changed after the function declaration.
Similarly, the following code prints 4
:
let x = 3;
function f() {
console.log(x);
}
x = 4;
f();
This can be solved using let
, which doesn't suffer from variable hoisting:
function setName() {
// vvv
for (let name of names) {
targetPages.push(function() {
console.log(name);
});
};
};
See MDN's "Creating closures in loops: a common mistake" for more information.
Upvotes: 2