Prerequisite: Understanding Javascript Scoping, Javascript Hoisting
In JavaScript, there are two types of scopes
- Global Scope: Scope outside the outermost function attached to the window.
- Local Scope: Inside the function being executed.
Hoisting: It is a concept that enables us to extract values of variables and functions even before initializing/assigning value without getting errors and this is happening due to the 1st phase (memory creation phase) of the Execution Context.
Do you know what value will be printed on the console when the following piece of code will be executed?
Javascript
<script> var x = 10; function test() { var x = 20; } test(); console.log(x); </script> |
Output:
10
If your answer is 10, then you are right. Here the variable ‘x’ declared (and of course initialized) outside the function ‘test’ has a global scope and that’s why it is accessible anywhere in this scope globally. However, the one declared and initialized inside the ‘test’ function can be accessed only inside that function.
So the below code snippet will print 20 on the console upon execution.
Javascript
<script> var x = 10; function test() { var x = 20; console.log(x); } test(); </script> |
Output:
20
Javascript
<script> var x = 10; function test() { if (x > 20) { var x = 50; } console.log(x); } test(); </script> |
Output:
undefined
If you have guessed 10 following the scoping logic discussed in the previous examples, then you are unfortunate as 10 is not the correct answer. This time it will print ‘undefined’ on the console. This is because of the combined effect of variable scoping and variable hoisting.
Scoping in JavaScript: Let us first understand scoping. The scope is a region of the program where a variable can be accessed. In other words, scope determines the accessibility/visibility of a variable. Since JavaScript looks like a C-family language, it is very obvious to think that scoping in JavaScript is similar to that in most of the back-end programming languages like C, C++, or Java.
Let’s consider the following code snippet written in C:
C
#include<stdio.h> void doSomething(); int main() { doSomething(); return 0; } // This function examplifies // the block-level-scope in C language void doSomething() { int x = 10; printf ( "%d\n" , x); if (x == 10) { int x = 20; printf ( "%d\n" , x); } printf ( "%d\n" , x); } |
Output :
10 20 10
Here the output is so, because C, as well as the rest of the C-family, has a block-level-scope. Whenever the control enters into a block, such as an if-block or a loop (e.g., for, while, or do-while), the language allows new variables to be declared that can be visible within that block only. Here the variables declared in the inner scope do not affect the value of the variables declared in the outer scope. But this is not the case in JavaScript.
Example: Let’s consider the following JavaScript example.
Javascript
<script> var x = 10; console.log(x); if ( true ) { var x = 20; console.log(x); } console.log(x); </script> |
Output :
10 20 20
Unlike the previous code, written in C, here the last output changes depending on the value assigned to the variable ‘x’ inside the if-block. This is because JavaScript does not have block-level scope. It has a function-level-scope. That means blocks such as if-statements and loops do not create new scopes in JavaScript. Rather a new scope is created only when a function is defined. This sometimes creates confusion for developers who are working with C, C++, C# or Java-like languages. Luckily, JavaScript allows function definitions to go inside any block. For example, we can write the above code by implementing an IIFE (Immediately Invoked Function Expression) inside the if-block that will result in a different output.
Javascript
<script> var x = 10; console.log(x); if ( true ) { ( function () { var x = 20; console.log(x); })(); } console.log(x); </script> |
Output :
10 20 10
In this case, when the control enters into the anonymous function defined and invoked inside the if-block, it creates a new scope. The variable ‘x’ declared inside this scope does not affect the value of the variable ‘x’ declared in the outer scope. Though this is a quite flexible way for creating temporary scopes wherever required, it is not qualified as a good coding style. Hence, to keep things simple ES6 has introduced two new keywords- ‘let’ and ‘const’, to declare block-scoped variables. When a variable is declared using ‘const’ or ‘let’, it is visible only inside the particular block in which it is declared. For example:
Javascript
<script> var x = 10; console.log(x); function test() { var x = 20; console.log(x); if (x > 10) { let x = 30; console.log(x); } console.log(x); } test(); console.log(x); </script> |
Output :
10 20 30 20 10
In this case, when the test() function is invoked the console.log() statement written inside the if-block prints 30 on the console, whereas the one that follows the if-block inside test() function prints 20 on the console. This means that the variable ‘x’ declared and defined using let keyword has no effect on the value of the variable ‘x’ declared outside its scope, i.e. the if-block. The keyword ‘const’ also operates in a similar manner. The only difference between ‘let’ and ‘const’ is- const is a signal that the identifier won’t be reassigned (The reason that the word ‘identifier’ is preferred here over ‘variable’ is that ‘const’ is used to declare the identifier, it is no longer a variable. It is a constant.), whereas let is a signal that the variable may be reassigned.
Now let’s go back to the following example provided previously in this article.
Javascript
<script> var x = 10; function test() { if (x > 20) { var x = 50; } console.log(x); } test(); </script> |
Output :
undefined
We have already discussed why in this example the console.log() statement did not print 10 on the console. Since JavaScript (especially the var keyword) has function-level-scope, the variable ‘x’ declared inside the if-block is visible throughout the function test(). So when the console.log() statement executes, it tries to print the value of the inner ‘x’ rather than the value of the one declared outside the function definition. Now the question is if this code snippet prints the value of the inner ‘x’ then why is it printing ‘undefined’? Here the inner ‘x’ is both declared and defined inside the if-block which is evaluated to false and in JavaScript an attempt to access a variable before its declaration results in a ReferenceError. Then how come the variable is even getting declared inside the function allowing it to be executed without any error? Does the function-level-scope make the conditional statements, like if-else, ineffective and if so, why is not it printing 50, the actual value of ‘x’ declared inside the function? All these questions have a single answer- hoisting.
Hoisting in Javascript: It is JavaScript’s default behavior of moving declarations to the top of their containing scope. When a JavaScript code is interpreted, the interpreter invisibly moves (hoist) all the variable and function declarations to the top of the scope they are declared in. However, the location of their definition/initialization/instantiation remains unaffected. For example, the above code snippet will be interpreted as the following before execution.
Javascript
<script> var x; x = 10; function test() { var x; if (x > 20) { x = 50; } console.log(x); } test(); </script> |
Output:
undefined
In this case, since no value is assigned to the variable ‘x’ declared at the top of the test() function, JavaScript automatically assigns the value ‘undefined’ to it. Since the if-condition is evaluated to false, the ‘console.log()’ statement prints ‘undefined’ on the console.
Now let’s look at another example:
Javascript
<script> function test() { if ( false ) { var x = 50; } console.log(x); console.log(y); var y = 100; console.log(y); } test(); </script> |
Output:
undefined undefined 100
This code is interpreted as the following.
Javascript
<script> function test() { var x, y; if ( false ) { x = 50; } console.log(x); console.log(y); y = 100; console.log(y); } test(); </script> |
Output:
undefined undefined 100
In the first line of the function body where ‘x’ and ‘y’ are declared in the interpreted code, JavaScript assigns ‘undefined’ to both variables. Since the if-condition is evaluated to false, the first two ‘console.log()’ statements print ‘undefined’ on the console. However, the last statement prints 100 as it is assigned to ‘y’ before the final ‘console.log()’ executes.
Now the important point here is, this variable hoisting mechanism works only for variables declared using var keyword. It does not work for variables or identifiers declared using let and const keywords respectively. Let’s consider the following example.
Javascript
<script> function test() { if ( false ) { let x = 50; } console.log(x); console.log(y); let y = 100; console.log(y); } test(); </script> |
Output:
ReferenceError: x is not defined
Identifiers declared using let or const are not at all hoisted. This makes them inaccessible before their declaration in the original source code.
Now a question may come- what is the best practice to declare variables and constants in JavaScript keeping variable hoisting in mind, so that it will never make our code return an unexpected result?
Here is the answer.
- All variables and constants that need to be visible within the scope of a particular function should be declared using ‘var’ and ‘const’ keywords respectively at the top of that function.
- Inside blocks (conditional statements or loops) variables and constants should be declared on the top of the block using ‘let’ and ‘const’ respectively.
- If in a particular scope multiple variables or constants need to be declared, then declare them in one go by using a single ‘var’ or ‘let’ or ‘const’ keyword with comma separated identifier names,
e.g.,
var x, y, z; // declaring function-scoped variables
let a, b, c; // declaring block-scoped variables
const u, v, w; // declaring block-scoped constantsThough the last point has nothing to do with the consequences of variable hoisting, it is a better practice to keep it in mind while writing code to keep the code clean.
Like variables, functions are also hoisted in JavaScript. However, how the hoisting will be done depends on the way a function is declared. JavaScript allows developers to define a function in two ways- function declaration and function expression.
Let’s consider the following example.
Javascript
<script> function test() { foo(); bar(); // Function defined // using function declaration function foo() { console.log( 'foo' ); } // Function defined // using function expression var bar = function () { console.log( 'bar' ); } } test(); </script> |
Output:
foo TypeError: bar is not a function
The JavaScript interpreter interprets the above code as follows.
Javascript
<script> function test() { function foo() {} var bar; foo(); bar(); // Function defined // using function declaration function foo() { console.log( 'foo' ); } // Function defined // using function expression bar = function () { console.log( 'bar' ); } } test(); </script> |
Output:
foo TypeError: bar is not a function
Since function foo() is defined using function declaration, the JavaScript interpreter moves its declaration to the top of its container scope, i.e., the body of test(), leaving its definition part behind. The definition is dynamically assigned when foo() is invoked.
This leads to the execution of the body of foo() function when the function is called. On the other hand, defining a function using function expression is nothing other than variable initialization where the function is treated as the value to be assigned to the variable. Therefore it follows the same hoisting rule which is applicable to variable declarations. Here the declaration of the variable ‘bar’ is moved to the top of its container scope while the place of assigning the function to it remains unchanged. JavaScript cannot interpret a variable as a function until it is assigned a value that is actually a function. This is why trying to execute the statement bar(); before ‘bar’ is defined results in a TypeError.
Though the decision regarding which way one should use to define a function solely depends on developer’s own choice, it is better to keep the following things in mind.
- Define all the functions using function declaration method on the top of their container scope. This not only keeps the code clean but also ensures that all the functions are declared and defined before they are invoked.
- If you must have to define a function using function expression method, then make sure that it is defined before it is invoked in the code. This will eliminate the chance of unexpected outputs or errors that may result due to hoisting.