
I wasted several hours debugging a problem last night in some JavaScript code, and now that (I think) I've got it sorted out, it's time to share the pain. The issue stemmed from my misunderstanding of how variable scoping works in JavaScript. The MDN docs on the var statement helped clear things up, but here's a few examples to demonstrate the problem.
Example 1
Let's declare a variable i
that's set to 5
, then call a function that defines another i
as part of a for
loop. Unsurprisingly, the inner loop prints out 1 through 9, while the first and last statements prints 5 (the value of the original i
variable).
function counter() {
for (var i = 0; i < 10; i++) {
document.writeln(i);
}
}
var i = 5;
document.writeln(i + '<br>'); // 5
counter(); // 0-9 (counter sets i to 0)
document.writeln('<br>' + i); // 5
// output:
// 5
// 0 1 2 3 4 5 6 7 8 9
// 5
No surprises so far. That did exactly what I'd expect it to do. But what if we leave the var
keyword out of the declaration of i
in the loop? My assumption was that it'd behave the same way... oops.
function counter() {
for (i = 0; i < 10; i++) {
document.writeln(i);
}
}
var i = 5;
document.writeln(i + '<br>'); // 5
counter(); // 0-9 (counter sets i to 0)
document.writeln('<br>' + i); // 5 ?
// output:
// 5
// 0 1 2 3 4 5 6 7 8 9
// 10
So what happened the second time around? The i
inside of counter()
was not redeclared (no var
this time), so it re-used the same i
declared outside of the function, which is a global variable.
The scope of a variable declared with
var
is its current execution context, which is either the enclosing function (our first example) or, for variables declared outside any function, global (our second example).
Example 2
Here's another example, where i
is declared outside of a loop, and then again inside the loop. So, after reading the above, I'd consider these 2 separate i
variables to be within different contexts. That'd be consistent with other languages.
var i = 5;
for (var i = 0; i <= 9; i++) {
document.writeln(i);
}
document.writeln('<br>' + i + '<br>');
// output:
// 0 1 2 3 4 5 6 7 8 9
// 10
Wrong again, apparently.
True to the docs, the different context seems to only occur at the function level. If we move the for
loop inside a function and call that, then the two i
variables are indeed treated differently. Leave out the var
keyword on the loop, however, and we'll end up with the same result as the second example in the last section.
var i = 5;
function loopFunc() {
for (var i = 0; i <= 9; i++) {
document.writeln(i);
}
}
loopFunc();
document.writeln('<br>' + i + '<br>');
// output:
// 0 1 2 3 4 5 6 7 8 9
// 5
Now With 200% More Solutions!
Update: After writing this up and lamenting on Twitter, I got two great responses with two great solutions.
Let
First, Dave Johnson told me about the let
keyword, which creates a new binding for the variable on every iteration, and separate from any other variable of the same name outside the loop.
So if you retry the first example in the previous section, substituting let
for var
, the answer is more inline with what you might expect in other languages.
var i = 5;
for (let i = 0; i <= 9; i++) {
document.writeln(i);
}
document.writeln('<br>' + i + '<br>');
// output:
// 0 1 2 3 4 5 6 7 8 9
// 5
Read more: Exploring ES6 - let
and const
in loop heads
Use Strict
A little while later, Axel Rauschmayer (author of the above article, which is part of a book) told me about strict mode, which causes an error to be thrown if a variable is not declared. It solves a slightly different problem.
The previous example would still produce the same result, but whereas we could completely forego declaring i
and it'll simply be created as a global variable for us, if we specify "use strict" then it'll throw an error instead!
"use strict";
try {
for (i = 0; i <= 9; i++) {
document.writeln(i);
}
document.writeln('<br>' + i + '<br>');
}
catch (err) {
document.writeln(err);
}
// output:
// ReferenceError: i is not defined
What'd I Learn?
Um, always read the docs? Heh.
JavaScript is quite a dynamic language, and full of surprises. I'd expect one of two things instead of what actually occurred:
- The
var
keyword is optional, and omitting it still creates a separatei
variable in its own scope. - The
var
keyword is required, and omitting it throws a syntax error.
What we actually get is that by omitting the var
keyword, it simply looks outside of the function for any identically named variable that was declared before the function was called, and uses that instead. We don't even have to mark the outer variable as global
- it simply is.
My actual problem was more complex, involving recursion... actually, I haven't exactly figured out why the problem was occurring where it was, but the solution was the same.
I had a function that used i
inside a loop without ever declaring it with var
, and the loop called the same function again. Even though i
was declared nowhere else in my script, somehow it led to the function being called recursively forever. Once I introduced the var
keyword, everything worked as expected and the function stopped being called recursively as soon as the condition that should stop it was true.
JavaScript. sigh...