A friend of mine and I are currently discussing what is a closure in JS and what isn't. We just want to make sure we really understand it correctly.
Let's take this example. We have a counting loop and want to print the counter variable on the console delayed. Therefore we use setTimeout
and closures to capture the value of the counter variable to make sure that it will not print N times the value N.
The wrong solution without closures or anything near to closures would be:
for(var i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
which will of course print 10 times the value of i
after the loop, namely 10.
So his attempt was:
for(var i = 0; i < 10; i++) {
(function(){
var i2 = i;
setTimeout(function(){
console.log(i2);
}, 1000)
})();
}
printing 0 to 9 as expected.
I told him that he isn't using a closure to capture i
, but he insists that he is. I proved that he doesn't use closures by putting the for loop body within another setTimeout
(passing his anonymous function to setTimeout
), printing 10 times 10 again. The same applies if I store his function in a var
and execute it after the loop, also printing 10 times 10. So my argument is that he doesn't really capture the value of i
, making his version not a closure.
My attempt was:
for(var i = 0; i < 10; i++) {
setTimeout((function(i2){
return function() {
console.log(i2);
}
})(i), 1000);
}
So I capture i
(named i2
within the closure), but now I return another function and pass this around. In my case, the function passed to setTimeout really captures i
.
Now who is using closures and who isn't?
Note that both solutions print 0 to 9 on the console delayed, so they solve the original problem, but we want to understand which of those two solutions uses closures to accomplish this.
Answer
Editor's Note: All functions in JavaScript are closures as explained in this post. However we are only interested in identifying a subset of these functions which are interesting from a theoretical point of view. Henceforth any reference to the word closure will refer to this subset of functions unless otherwise stated.
A simple explanation for closures:
- Take a function. Let's call it F.
- List all the variables of F.
- The variables may be of two types:
- Local variables (bound variables)
- Non-local variables (free variables)
- If F has no free variables then it cannot be a closure.
- If F has any free variables (which are defined in a parent scope of F) then:
- There must be only one parent scope of F to which a free variable is bound.
- If F is referenced from outside that parent scope, then it becomes a closure for that free variable.
- That free variable is called an upvalue of the closure F.
Now let's use this to figure out who uses closures and who doesn't (for the sake of explanation I have named the functions):
Case 1: Your Friend's Program
for (var i = 0; i < 10; i++) {
(function f() {
var i2 = i;
setTimeout(function g() {
console.log(i2);
}, 1000);
})();
}
In the above program there are two functions: f
and g
. Let's see if they are closures:
For f
:
- List the variables:
i2
is a local variable.i
is a free variable.setTimeout
is a free variable.g
is a local variable.console
is a free variable.
- Find the parent scope to which each free variable is bound:
i
is bound to the global scope.setTimeout
is bound to the global scope.console
is bound to the global scope.
- In which scope is the function referenced? The global scope.
- Hence
i
is not closed over byf
. - Hence
setTimeout
is not closed over byf
. - Hence
console
is not closed over byf
.
- Hence
Thus the function f
is not a closure.
For g
:
- List the variables:
console
is a free variable.i2
is a free variable.
- Find the parent scope to which each free variable is bound:
console
is bound to the global scope.i2
is bound to the scope off
.
- In which scope is the function referenced? The scope of
setTimeout
.- Hence
console
is not closed over byg
. - Hence
i2
is closed over byg
.
- Hence
Thus the function g
is a closure for the free variable i2
(which is an upvalue for g
) when it's referenced from within setTimeout
.
Bad for you: Your friend is using a closure. The inner function is a closure.
Case 2: Your Program
for (var i = 0; i < 10; i++) {
setTimeout((function f(i2) {
return function g() {
console.log(i2);
};
})(i), 1000);
}
In the above program there are two functions: f
and g
. Let's see if they are closures:
For f
:
- List the variables:
i2
is a local variable.g
is a local variable.console
is a free variable.
- Find the parent scope to which each free variable is bound:
console
is bound to the global scope.
- In which scope is the function referenced? The global scope.
- Hence
console
is not closed over byf
.
- Hence
Thus the function f
is not a closure.
For g
:
- List the variables:
console
is a free variable.i2
is a free variable.
- Find the parent scope to which each free variable is bound:
console
is bound to the global scope.i2
is bound to the scope off
.
- In which scope is the function referenced? The scope of
setTimeout
.- Hence
console
is not closed over byg
. - Hence
i2
is closed over byg
.
- Hence
Thus the function g
is a closure for the free variable i2
(which is an upvalue for g
) when it's referenced from within setTimeout
.
Good for you: You are using a closure. The inner function is a closure.
So both you and your friend are using closures. Stop arguing. I hope I cleared the concept of closures and how to identify them for the both of you.
Edit: A simple explanation as to why are all functions closures (credits @Peter):
First let's consider the following program (it's the control):
lexicalScope();
function lexicalScope() {
var message = "This is the control. You should be able to see this message being alerted.";
regularFunction();
function regularFunction() {
alert(eval("message"));
}
}
- We know that both
lexicalScope
andregularFunction
aren't closures from the above definition. - When we execute the program we expect
message
to be alerted becauseregularFunction
is not a closure (i.e. it has access to all the variables in its parent scope - includingmessage
). - When we execute the program we observe that
message
is indeed alerted.
Next let's consider the following program (it's the alternative):
var closureFunction = lexicalScope();
closureFunction();
function lexicalScope() {
var message = "This is the alternative. If you see this message being alerted then in means that every function in JavaScript is a closure.";
return function closureFunction() {
alert(eval("message"));
};
}
- We know that only
closureFunction
is a closure from the above definition. - When we execute the program we expect
message
not to be alerted becauseclosureFunction
is a closure (i.e. it only has access to all its non-local variables at the time the function is created (see this answer) - this does not includemessage
). - When we execute the program we observe that
message
is actually being alerted.
What do we infer from this?
- JavaScript interpreters do not treat closures differently from the way they treat other functions.
- Every function carries its scope chain along with it. Closures don't have a separate referencing environment.
- A closure is just like every other function. We just call them closures when they are referenced in a scope outside the scope to which they belong because this is an interesting case.
No comments:
Post a Comment