In the world of JavaScript, understanding the concept of hoisting can significantly improve the way you write and debug your code. Hoisting is one of those interesting behaviors of JavaScript that can sometimes lead to unexpected results, especially for those new to the language. In this article, we'll explore what hoisting is, how it works, and look at some examples to get a clearer picture.
TOC
- Variable Hoisting
- Function Hoisting
- Class Hoisting
- Type Alias & Interface "Hoisting" in TypeScript
- Tricky Tasks
- Solutions
Variable Hoisting
Variable hoisting is a JavaScript mechanism where variable declarations are moved to the top of their containing scope before the code has been executed. This means that no matter where variables are declared, they are moved to the top of their scope by the JavaScript interpreter.
Importantly, only the declarations are hoisted, not the initializations. This distinction is crucial for understanding some of the puzzling behavior that can arise.
To illustrate how variable hoisting works, let's look at a few examples.
Example 1: Basic Variable Hoisting
console.log(myVar); // Output: undefined
var myVar = 5;
In this example, you might expect a ReferenceError since myVar
is printed before it's declared. However, due to hoisting, the JavaScript interpreter sees the code as follows:
var myVar;
console.log(myVar); // Output: undefined
myVar = 5;
Example 2: let
and const
Hoisting
The introduction of let
and const
in ES6 has somewhat altered the landscape of hoisting. Unlike var
, which is hoisted to the top of its scope, let
and const
are hoisted to the top of their block scope but are not initialized, leading to what is called a "Temporal Dead Zone" (TDZ).
console.log(myLetVar); // ReferenceError: Cannot access 'myLetVar' before initialization
let myLetVar = 3;
In this scenario, myLetVar
is hoisted, but trying to access it before declaration results in a ReferenceError.
Example 3: Re-declaring Variables with var
var myVar = 1;
console.log(myVar); // Output: 1
var myVar = 2;
console.log(myVar); // Output: 2
In this example, despite myVar
being declared twice, JavaScript's hoisting mechanism processes and consolidates the declarations at the top of their scope. The reassignment of myVar
to 2
happens after the initial assignment, leading to the final value of 2
being logged. Below is how the JavaScript engine interprets the code as if it's written like this:
var myVar;
myVar = 1;
console.log(myVar);
myVar = 2;
console.log(myVar);
Function Hoisting
In JavaScript, functions are subject to hoisting much like variables are. However, how function hoisting works depends on how the function is declared.
Example 4: Function Declaration Hoisting
Function declarations are fully hoisted. This means that the entire body of the function, along with its declaration, is hoisted to the top of its scope. Here's how it works:
console.log(myFunc()); // Output: "Some text!"
function myFunc() {
return "Some text!";
}
Example 5: Function Expression Hoisting
Function expressions behave differently. If a function is assigned to a variable, the variable declaration is hoisted but not the assignment. This distinction is crucial for understanding how different function declaration methods behave regarding hoisting.
Consider this example using a function expression with var
:
console.log(myFunc); // Output: undefined
console.log(myFunc()); // TypeError: myFunc is not a function
var myFunc = function() {
return "Some text!";
};
Here, the variable myFunc
is hoisted to the top of its scope, meaning it exists and is undefined
when we try to log it or call it. This results in a TypeError when attempting to invoke myFunc
as a function before the function expression is assigned to it.
For function expressions using let
or const
, an attempt to use them before declaration will lead to a ReferenceError because let
and const
declarations are in a TDZ as described earlier.
Class Hoisting
Classes in JavaScript exhibit the same hoisting behavior to variables declared with let
and const
. When you declare a class, its declaration is hoisted to the top of its enclosing scope, much like let
and const
. However, just as let
and const
declarations are not initialized until the JavaScript engine executes their declaration line, classes too are not initialized until the engine evaluates their declaration. This also results in a TDZ, meaning any attempts to access the class before its declaration will result in a ReferenceError
. So, dealing with inheritance, static methods, and other object-oriented programming constructs, ensure that base classes are defined before derived classes try to extend them, respecting the TDZ.
Type Alias & Interface "Hoisting" in TypeScript
TypeScript's type aliases and interfaces are purely a design-time construct, meaning they are used by the TypeScript compiler for type checking and are erased when the TypeScript code is compiled to JavaScript. So, the concept of hoisting doesn’t really apply to type aliases or interfaces because they do not exist in the compiled output and, therefore, have no impact on the runtime behavior of the code.
For example:
let myVar: MyType = { key: "value" };
type Animal = {
key: string;
};
In the code above, even though the MyType
type is used before its declaration, TypeScript does not throw an error. This is because, from the perspective of the TypeScript type checking system, all type declarations are considered to be known ahead of any value-space code execution or evaluation. This isn't hoisting in the traditional JavaScript sense, but rather a feature of TypeScript's static analysis that all type declarations are globally known to the compiler before any code execution.
Tricky Tasks
Task 1.
var myVar = 1;
var myFunc = function() {
console.log(myVar); // Output: ?
var myVar = 2;
console.log(myVar); // Output: ?
}
myFunc();
Task 2.
function myFunc() {
return 1;
}
console.log(myFunc()); // Output: ?
function myFunc() {
return 2;
}
console.log(myFunc()); // Output: ?
Task 3.
(function() {
try {
throw new Error();
} catch (x) {
var x = 1, y = 2;
console.log(x); // Output: ?
}
console.log(x); // Output: ?
console.log(y); // Output: ?
})();
Solutions
Task 1.
- When
myFunc
is invoked, JavaScript hoists the declaration ofmyVar
to the top of the function scope. This means the code insidemyFunc
behaves as if it was written like this:
var myVar = undefined;
console.log(myVar); // Output: undefined
myVar = 2;
console.log(myVar); // Output: 2
- Therefore, the first
console.log(myVar)
outputsundefined
because the declaration (but not the initialization) ofmyVar
is hoisted to the top of the function scope, overshadowing themyVar
declared outside the function scope. - After
myVar
is assigned the value of2
within the function, the secondconsole.log(myVar)
outputs2
.
Task 2.
- Both function declarations are hoisted to the top of their scope. However, the second declaration of
myFunc
overwrites the first. This is because in JavaScript, function declarations are hoisted above variable declarations and are processed before the code begins to run. - Therefore, regardless of where the functions are declared, the version of
myFunc
that finally resides in memory after hoisting and interpretation is the one that returns2
. - Both
console.log(myFunc());
calls will output2
, because that's the version ofmyFunc
being used after hoisting and overwriting.
Task 3.
- In the
catch
block, variablesmyVar1
andmyVar2
are declared. Sincevar
declarations are function-scoped (not block-scoped likelet
orconst
), bothmyVar1
andmyVar2
are hoisted to the top of their enclosing function. - Therefore,
console.log(myVar1);
within thecatch
block outputs1
. - Outside the
catch
block but inside the self-invoking function, bothmyVar1
andmyVar2
are accessible due to hoisting, and since they were assigned values in thecatch
block, their values are preserved outside of the catch block. -
console.log(myVar1);
outside thecatch
block also outputs1
. - Similarly,
console.log(myVar2);
outputs2
.
Top comments (0)