On JavaScript code blocks, variable scope and variable hoisting
I am currently reading the book JavaScript Patterns, by Stoyan Stefanov and I came across this interesting tidbit of information regarding variable scopes in JavaScript. Consider the following C++ code:
#include <iostream>
int main() {
if(true) {
int foo = 10;
}
std::cout<<foo;
return 0;
}
Compiling this produces the following error:
scope.cpp(8) : error C2065: 'foo' : undeclared identifier
This is only to be expected. In C++, code blocks automatically define a variable scope, i.e., variables defined inside the code block are not visible outside of it. The same behavior can be observed in many of the other popular programming languages in use today such as C# and Java with one notable exception - JavaScript! Consider the following JavaScript snippet:
function scope() {
if(true) {
var foo = 10;
}
print(foo);
}
scope();
Interestingly, this prints the value "10"! What's going on here? The first thing one needs to understand about JavaScript is the notion of "variable hoisting". Regardless of where you have declared a variable, the runtime treats it as though it has been declared at the start of the scope. Here's an example:
function hoist() {
try {
print(undeclaredVariable === undefined);
print("Yep, was able to refer to 'undeclaredVariable'.");
}
catch(e) {
print("Could not use 'undeclaredVariable'.");
}
try {
print(declaredLater === undefined);
print("Yep, was able to refer to 'declaredLater'.");
}
catch(e) {
print("Could not use 'declaredLater'.");
}
var declaredLater = 10;
}
hoist();
Running this snippet produces the following output:
Could not use 'undeclaredVariable'.
true
Yep, was able to refer to 'declaredLater'.
In the function "hoist" we have declared a variable named "declaredLater" right at the end of the function body. Interestingly, while we were unable to refer to an undeclared variable (evidenced by the fact that the "print" statement inside the first "catch" block got executed) we seem to have no problems referring to "declaredLater" even before it is actually declared. This is so because the JavaScript runtime "hoists" all the local variables declared in the function to the top of the function. It is as though the function had been defined like so:
function hoist() {
var declaredLater;
// stuff here
declaredLater = 10;
}
hoist();
Note that though the variable itself gets declared at the top of the function, the initialization still happens only on the line where it occurred in the original function - which explains why the statement print(undeclaredVariable === undefined)
ends up printing true
.
Now, why did our "scope" function work the way it did? Turns out that in JavaScript curly braces do not introduce a variable scope. Only functions do. This means that variables declared inside of "if", "for", "while" etc. behave as though they have been declared at the top of the function. The aforementioned book therefore recommends moving all variable declarations to the top of the function. So, instead of doing the following:
function foo() {
var a = [1, 2, 3, 4];
if(someCondition() === true) {
var b, c, d;
// do something with b, c, d
}
for(var i = 0; i < a.length ; ++i) {
// do something with "a"
}
}
Do something like the following:
function foo() {
var a = [1, 2, 3, 4], b, c, d, i, len = a.length;
if(someCondition() === true) {
// do something with b, c, d
}
for(i = 0; i < len ; ++i) {
// do something with "a"
}
}
This makes it clear that the scope of the variables in question is the function "foo".
The key takeaways therefore are the following:
- Code blocks do not determine variable scope in JavaScript. Functions are the only construct that can be used to limit scope of variables.
- Regardless of where variables are declared, the runtime always treats them as though they have been declared at the beginning of the function in question.
If you need some global code to be executed, then use an immediate function (also known as a "self-executing" function) to limit the scope of the local variables you use in that code. So instead of doing the following:
<script> var v1, v2; // some code here that uses v1 and v2 </script>
Use code like this:
<script> (function() { var v1, v2; // some code here that uses v1 and v2 })(); </script>