JavaScript Hoisting and Scope

Hoisting is JavaScript’s default behavior of moving all declarations to the top of the current scope (to the top of the current script or the current function). In different words, a variable can be used before it has been declared. Look at the two examples below:

Example 1


var foo; // declare foo
foo = 5; // assign 5 to foo

console.log(foo); // displays 5 

Example 2

bar = 5; // assign 5 to foo

console.log(bar); // displays 5 

var bar; // declare foo
<p>Both example 1 and Example 2 produce the same result.</p>

JavaScript Initializations are Not Hoisted

JavaScript only hoists declarations, not initializations.

Example 1

var x = 5; // Initialize x
var y = 7; // Initialize y

elem = document.getElementById("demo"); // Find an element 
elem.innerHTML = x + " " + y;           // Display x and y

Example 2

var x = 5; // Initialize x

elem = document.getElementById("demo"); // Find an element 
elem.innerHTML = x + " " + y;           // Display x and y

var y = 7; // Initialize y         // Display x and y

Example 1 and Example 2 does not produce the same result.

Hers’an example that might surprice you:

var a = 1;
function b() {
	a = 10;
	return;
	function a() {}
}
b();
console.log(a);

Here, of course, the browser will return 1. Even though this might seem strange and confusing, this is actually hoisting in action, a powerful feature of JavaScript. To understand hoisting, first lets take a necessary detour to understand JavaScript’s scoping.

Scoping in JavaScript

Block Level Scoping vs Fucntional Level Scoping

One reason of confusion for JavaScript programmers is scoping. The reason scoping is so confusing in JavaScript is because it looks like a C-family language, and seem to have block level scoping. Consider the following C program:

#include <stdio.h>
int main() {
	int x = 1;
	printf("%d, ", x); // 1
	if (1) {
		int x = 2;
		printf("%d, ", x); // 2
	}
	printf("%d\n", x); // 1
}

The output from this program will be 1, 2, 1. This is because C, and the rest of the C family, has block-level scope. When control enters a block, such as the if statement, new variables can be declared within that scope, without affecting the outer scope. This is not the case in JavaScript. Try the following in Firebug:

var x = 1;
console.log(x); // 1
if (true) {
	var x = 2;
	console.log(x); // 2
}
console.log(x); // 2

In this case, Firebug will show 1, 2, 2. This is because JavaScript has function-level scope. This is radically different from the C family. Blocks, such as if statements, do not create a new scope. Simply accessing variables outside of your immediate lexical scope creates a closure in JavaScript. Only functions create a new scope.

To a lot of programmers who are used to languages C family, this is confusing. Luckily, because of the flexibility of JavaScript functions, there is a workaround. If you must create temporary scopes within a function, do the following:

function foo() {
	var x = 1;
	if (x) {
		(function () {
			var x = 2;
			// some other code
		}());
	}
	// x is still 1.
}

This method is actually quite flexible, and can be used anywhere you need a temporary scope, not just within block statements. This is quite powerful, and if you understand scoping, hoisting will make a lot more sense to you.

Declarations, Names, and Hoisting

In JavaScript, a name enters a scope in one of four basic ways:

  1. Language-defined: All scopes are, by default, given the names this and arguments.
  2. Formal parameters: Functions can have named formal parameters, which are scoped to the body of that function.
  3. Function declarations: These are of the form function foo() {}.
  4. Variable declarations: These take the form var foo;.

Function declarations and variable declarations are always moved (“hoisted”) invisibly to the top of their containing scope by the JavaScript interpreter. Function parameters and language-defined names are, obviously, already there. This means that code like this:

function foo() {
	bar();
	var x = 1;
}
is actually interpreted like this:

function foo() {
	var x;
	bar();
	x = 1;
}

It turns out that it doesn’t matter whether the line that contains the declaration would ever be executed. The following two functions are equivalent:

function foo() {
	if (false) {
		var x = 1;
	}
	return;
	var y = 1;
}
function foo() {
	var x, y;
	if (false) {
		x = 1;
	}
	return;
	y = 1;
}

Notice that the assignment portion of the declarations were not hoisted. Only the name is hoisted. This is not the case with function declarations, where the entire function body will be hoisted as well. But remember that there are two normal ways to declare functions. Consider the following JavaScript:

function test() {
	foo(); // TypeError "foo is not a function"
	bar(); // "this will run!"
	var foo = function () { // function expression assigned to local variable 'foo'
		alert("this won't run!");
	}
	function bar() { // function declaration, given the name 'bar'
		alert("this will run!");
	}
}

test();

In this case, only the function declaration has its body hoisted to the top. The name ‘foo’ is hoisted, but the body is left behind, to be assigned during execution.

That covers the basics of hoisting, which is not as complex or confusing as it seems. Of course, this being JavaScript, there is a little more complexity in certain special cases.

How to Code With This Knowledge

The most important thing is to always declare your variables with a var statement. I strongly recommend that you have exactly one var statement per scope, and that it be at the top. If you force yourself to do this, you will never have hoisting-related confusion. However, doing this can make it hard to keep track of which variables have actually been declared in the current scope. I recommend using JSLint with the onevar option to enforce this. If you’ve done all of this, your code should look something like this:

/*jslint onevar: true [...] */
function foo(a, b, c) {
    var x = 1,
    	bar,
    	baz = "something";
}

What the Standard Says

I find that it’s often useful to just consult the ECMAScript Standard directly to understand how these things work.

 

Be first to comment

Leave a Reply