Hoisting in JavaScript
Hoisting is a fundamental concept of the JavaScript language. It's also one of those topics which is frequently asked in interviews.
While the concept is very simple it is mostly misunderstood. Let's understand hoisting with the help of some examples.
Consider this example:
var source = "tweaked.dev";
console.log(source);
function printMe() {
console.log("from printMe:" + source);
}
printMe();
// Output:
// tweaked.dev
// from printMe:tweaked.dev
It's pretty straight forward right?
The code is executed line by line.
Let's take a look at what's happening:
- The variable source is created and assigned a value "tweaked.dev".
- console.log statement is encountered and the value of source is printed on the console → tweaked.dev
- Similarly the next line is printMe function definition. Followed by the actual call to the printMe() function. This results in the statements inside the function being executed and the string "from printMe:tweaked.dev" being printed to the console.
But is code execution really this straight forward in JavaScript?
Take a look at this variation of the same example.
console.log(source);
printMe();
function printMe() {
// function definition
console.log("from printMe:" + source);
}
var source = "tweaked.dev"; // variable declaration and value assignment
printMe();
Here we are logging the variable source to console even before it is declared.
Also we are calling the function printMe before it is being defined.
Will this code throw an error?
What do you think is the expected output for this example?
Take a moment to think before you see the output below.
console.log(source);
printMe();
function printMe() {
console.log("from printMe:" + source);
}
var source = "tweaked.dev";
printMe();
// Output:
// undefined
// from printMe:undefined
// from printMe:tweaked.dev
In most programming languages this would throw an exception.
Well, JavaScript allows this. How? Due to hoisting.
Hoisting is a feature in javascript where a variable can be used before it is declared. Similarly a function can also be called before it is defined.
To understand how hoisting works we need to understand the execution context.
Execution Context
Execution context is the environment which is prepared by the javascript engine in order to execute the code that you write.
Simply put it is the information which is necessary in order to execute the code.
The execution context is created in two phases :-
Creation Phase
- Code is scanned/parsed for variables and functions.
- Space is reserved for variables in memory.
- Function definitions are loaded in memory.
Execution Phase
- Code is executed statement by statement with the information from the creation phase.
- Variables are assigned values.
- Functions are executed.
Let's see the execution context phases for the example illustrated earlier:
It finds the variable source and loads it in the memory. Since the code is not yet executed it's value is undefined.
The function printMe is also loaded in memory.
To check if you've clearly understood the concept of hoisting and execution context, let’s consider another example:
source = 5;
console.log(source);
printMeFnExp();
// function expression syntax
var printMeFnExp = function printMe() {
console.log("from function:" + source);
};
var source;
// Output:
// 5
// Uncaught TypeError: printMeFnExp is not a function
Surprised by the error in the output? You shouldn't be.
For a better understanding, take a look at the diagrams below.
- The variables source and printMeFnExp are loaded in memory.
- The function printMe is loaded in the memory.
Take a closer look at printMeFnExp - it's a function expression. This indicates that it is variable whose value is pointing to a function.
In simple terms, the function is assigned to a variable. To call the function which is assigned to a variable, we need to write "functionName" followed by brackets.
Example: printMeFnExp()
- The value 5 is assigned to source.
- 5 is logged to the console.
- printMeFnExp is called. However it throws an error - Uncaught TypeError: printMeFnExp is not a function.
This is happening because the variable was hoisted, but it's initial value is still undefined. Thus we get an error for trying to call a function on an undefined value.
The statement which assigns the printMe function reference to printMeFnExp has not been executed.
To fix this error, see the code changes below:
source = 5;
console.log(source);
// function expression syntax
var printMeFnExp = function printMe() {
console.log("from function:" + source);
};
printMeFnExp();
var source;
// Output:
// 5
// from function:5
Here the printMeFnExp has been assigned the printMe function reference. Thus it is now possible to invoke the function expression like this - printMeFnExp();
Note: There is more to the execution context than mentioned in this article. I've covered just enough for you to understand hoisting.
Exceptions to hoisting
Hoisting works differently if the variable declaration is done using let or const.
In case of var the value is initialized to undefined during the creation phase.
However in case of let and const the value is only intialized during the execution phase.
See the example below:
console.log(source);
printMe();
function printMe() {
console.log("from printMe:" + source);
}
let source = "tweaked.dev";
// Output:
// Uncaught ReferenceError: source is not defined
Since the value of source is not initialized during the creation phase, source has no reference to the value in memory.
Due to this a reference error is thrown for statment console.log(source);
This concept is also known as Temporal Dead Zone. It means that a variable cannot be accessed until it is declared.
Let's look at the same example with this knowledge.
// Temporal Dead Zone
///////////////////////////////////////////////
console.log(source); ///
printMe(); ///
///
function printMe() { ///
console.log("from printMe:" + source); ///
} ///
///
///////////////////////////////////////////////
let source = "tweaked.dev"; // Temporal Dead Zone Ends
Here the // lines represent the Temporal Dead Zone for the source variable. We will get a reference error if we try to access source within this block.
Below is the correct usage of let:
let source = "tweaked.dev";
console.log(source);
function printMe() {
console.log("from printMe:" + source);
}
printMe();
During the execution phase if no value is provided along with declaration, then the value is considered as undefined.
Refer the example:
let source; // declaration
console.log(source);
source = "tweaked.dev"; // initialization
function printMe() {
console.log("from printMe:" + source);
}
printMe();
// Output:
// undefined
// from printMe:tweaked.dev
Example with const:
const source;
console.log(source);
// Output:
// Uncaught SyntaxError: Missing initializer in const declaration
A const indicates a constant value. This value cannot be changed during code execution.
Therefore it makes sense that it requires an initializer value at the time of declaration.
Correct usage of const:
const source = "tweaked.dev";
console.log(source);
// Output:
// tweaked.dev
Conclusion
Coding best practices suggest to declare variables at the beginning of the code block.
It is also preferable to use let and const for variable declaration. This enhances code readability.
While the usage of variable declaration using var has become outdated, it is still important to know this concept since you might still encounter this pattern in some of the existing code bases or even an interview. There is a chance you might even have to refactor such a code with the latest JavaScript features.
Knowledge of hoisting will help you avoid any bugs and confusion related to variable declaration and its usage.
Please feel free to ask your queries or doubts.
You can read more articles from me on my personal blog - www.gauravsen.com/blog