JavaScript is a language that demands a rigorous understanding of its scoping rules. One reason for this is JavaScript looks deceptively like other languages, but subtle differences in the rules that govern identifier visibility make it unexpectedly difficult to master.
Here's some computer science so you can impress your friends and wayward wizards: scope refers to the visibility of a given identifier within a program. Sometimes we talk about function scope and block scope. I've already covered function scope in this series, but JavaScript doesn't have block scope. In this context, block refers to control structures like if
statements and for
loops -- a block of statements grouped by curly braces.
Declaring Variables and Functions
Variables can be defined with the var
statement, and are initialised to undefined
:
var a; // undefined
var b = 'hello';
The scoping of these statements is dependent on where they're declared. If var
statements don't appear inside a function, they're globally accessible:
var a = 1;
function sum(b) {
return a + b;
}
Missing var
Problems start to occur when a var
statement is forgotten:
function example() {
a = 1;
b = 1;
return a + b;
}
These variables are not local to the example
function, they're actually global. If a
or b
already existed, then their values will be overwritten.
Accidentally leaving out a var
statement is surprisingly easy, and could potentially cause irritating bugs. Tools like JSLint will attempt to use static analysis to find such errors.
Variable Declaration Styles
Some people like to group var
statements together:
var a = 1,
b = 2,
c = 3;
What would happen if a comma was missed by mistake?
function example() {
var a = 1
b = 2,
c = 3;
}
example();
console.log(typeof a);
console.log(typeof b);
console.log(typeof c);
Running this will show that while a
is undefined
and therefore local to example
, the other variables are global.
The reason some developers place commas before lists of variable declarations is to make it easier to see if a comma has been forgotten:
var a = 1
, b = 2
, c = 3
;
However, the Google JavaScript Style Guide recommends using a var
statement on every line, to avoid the problem altogether.
No Block Scope
Variables and functions are visible within the current function, regardless of blocks. This is amazingly confusing because it prevents us from using control structures to declare functions and variables in a dynamic way.
Defining variables in blocks may confuse programmers who work with other languages:
function example() {
// Do not do this
for (var i = 0; i < 3; i++) {
var a = 1;
// Do stuff with `a`
}
}
Since there is no block scope, the previous example should be written like this:
function example() {
var i, a;
for (i = 0; i < 3; i++) {
a = 1;
// Do stuff with `a`
}
}
Hoisting
Have you ever noticed how some things appear to be in scope even though their definition appears later in the file? The colloquial term for this is hoisting:
function example() {
console.log(a);
var a = 1;
}
example();
Running this will log undefined
rather than 1
. The term hoisting isn't in the ECMAScript 3 or 5 standards, but the behaviour is documented in 10.5 Declaration Binding Instantiation, in the line that starts For each _VariableDeclaration and VariableDeclarationNoIn d in code, in source text order do_. The reason the value is undefined
rather than 1
is also explained by the specification:
A variable with an Initialiser is assigned the value of its AssignmentExpression when the VariableStatement is executed, not when the variable is created.
Confusing Closures
The following example defines three functions and assigns them as methods to an object. Each method is then called after the loop has finished.
function example() {
var o = {}, i = 0;
for (i = 0; i < 3; i++) {
o[i] = function() { console.log(i); };
}
o[0]();
o[1]();
o[2]();
}
example();
The output will be 3
each time, because the closure is bound to the function scope, not the (non-existent) block scope. Intermediate JavaScript programmers often make this mistake.
Conclusion
If understanding JavaScript's scoping rules was a video game, then the levels would be as follows:
- Title Screen: Press [Start] to begin (I always press the other buttons to see if it starts anyway)
- Level 1: Function scope
- Level 2: No block scope
- Level 3: Missing var
- Level 4: Hoisting
- Level 5: Closures
The scoping story isn't complete, and worth noting is the fact that ECMAScript extensions can subtly alter scoping behaviour for certain things. If you're interested in going beyond this 101 introduction level article, then try reading Angus Croll's Function Declarations vs. Function Expressions to see how some of these scoping rules play out, and Dmitry Soshnikov's post on ECMAScript's lexical environments.
References
- Code Conventions for the JavaScript Programming Language by Douglas Crockford
- ECMAScript 5: 10.5 Declaration Binding Instantiation
- JSLint