Hoisting for the whole life
During technical interviews, JavaScript developers often face questions and coding tasks related to hoisting, the temporal dead zone, and variable declarations, among other topics. This focus is no coincidence - misunderstanding these fundamental concepts can lead to numerous bugs, while also reducing code readability and maintainability.
But what's most surprising is that many of the answers and even explanations on reputable resources vary greatly. In my opinion, the issue lies in the inconsistent use of terms among many developers and that the term hoisting is not formally defined in the ECMAScript specification.
Today, I will attempt to provide a clear, structured explanation and define together with you the term hoisting to help clarify your understanding.
Before I need to describe how hoisting behavior looks like with simple examples:
function testFn() {
console.log(A); // undefined
var A = 'A';
console.log(A); // 'A'
console.log(B); // Throw error: "Cannot access 'B' before initialization"
let B = 'B';
}
testFn()
It looks that interpreter has access to a variable before the place it was defined in the code, but without expected value. But for let there is an another behavior, interpreter knows about variable, but throw the error, cause variables wasn't initialized.
console.log(testFn); //function testFn() {...}
function testFn() {
console.log(A);
}
In this case interpreter has access to a variable and it's value before the place it was defined in the code.
It's time to figure out what is going on!
Let's start
From a brief review how JavaScript (ECMAScript) works.
Memory divided into two areas:
- Memory stack (for primitives)
- Memory heap (for object)
There is a callstack as any stack it's a data structure that works by principle Last In First Out (LIFO). JavaScript interpreter take and executes items from callstack synchronously one by one.
What is the item in the callstack?
This item is an execution context.
The execution context is a special internal abstraction data structure (the specification mechanism) that contains information about the function call. It includes the specific place in the code where the interpreter is located, the local variables of the function and other service information.
Note: It cannot be accessed directly. One function call has exactly one execution context associated with it.
When a function makes a nested call, the following happens:
- The execution of the current function is suspended.
- The execution context associated with it is stored in the execution context stack.
- The nested calls are executed, each of which has its own execution context.
- After they complete, the old context is popped from the stack, and execution of the outer function resumes from where it left off.
We could say that there are two kinds of execution contexts:
- global: This is the context in which all initial JavaScript code runs when the file is loaded into the browser. Any global code that is not within a function is executed in the global execution context.
- local (functional): This is the execution context that is created at the moment a function is called.
It means that the callstack literally is a execution context stack. The running (current) executing context is always the top element of the callstack and there can only be one.
There are three lifecycle phases that every execution context has:
- Initial phase: The creation phase occurs when a function is called, but before it is executed. At this stage, the JavaScript engine scans the function code, creates a environment records, and could initializes it (i.e., writes down all arguments, variables, internal functions, and then defines a reference to the outer lexical environment and sets the value of this).
- Execution phase: The code is being executed, JavaScript may create other execution contexts. While JavaScript is busy creating and executing nested execution contexts, the current one is waiting in callstack.
- Destruction phase: After all code within the current execution context has completed execution, it is popped off the callstack and destroyed.
Now, we have almost created foundation for the term of hosting, but before we should discuss one more thing.
Variables
A variable is an abstract storage location paired with an associated symbolic name
In other word this is a named storage for data.
Before the ECMAScript5 (ES5) standard came along, variables in JavaScript could be created without any keywords. Than we started use var and later with ECMAScript6 (ES6) const and let.
But, if we follow the definition given above, we create variables not only explicitly using key var, const and let but everywhere where we use declarative names:
function myFunction(name){ // myFunction variable
console.log(name) // name local variable - the same like var declaration
}
class MyClass { // MyClass variable
...
}
export { myFunction as greatFn }; // greatFn local variable inside the module
import { greatFn as notGreatFn } from './module.js'; //notGreatFn local variable outside the module
There are some important variable behavior characteristics we could define:
- Declaration time: When a variable is created (reserved place in memory)
- Initialization/Assigning time: When a variable value is assigned (placed in memory)
- Multiple creation: Variable could be created multiple times
- Mutability: The value of a variable can be reassigned
- Scope: Block, Functional, Module, Global
- Side effects
This table summarizes and describes the differences in variable behavior.
| Variable characteristics | var | let | const | function | Class | export | import |
|---|---|---|---|---|---|---|---|
| Declaration time | Initial phase | Initial phase | Initial phase | Initial phase | Initial phase | Initial phase | Initial phase |
| Initialization/Assigning time | Initial phase (undefined), Execution phase (with value) | Execution phase (with value) | Execution phase (with value) | Initial phase | Initial phase | Initial phase | Initial phase |
| Multiple creation | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
| Mutability | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ |
| Scope | Block, Functional, Module, Global | Block, Functional, Module, Global | may only appear at the top level | may only appear at the top level | |||
| Side effects | When variables are created without 'strict mode', they settle in the global browser object (window) | ❌ | ❌ | ❌ | ❌ | Once a module is imported for the first time, its exports are cached | Once a module is imported for the first time, its exports are cached |
Now we have everything we need!
Hoisting
Let's define the term:
Hosting is a JavaScript mechanism when a variable declaration is proceeding during the execution context Initial phase.
It's pretty simple and clear!
You could check that this term works for all variables that I mention before:
| Characteristics | var | let | const | function | Class | export | import |
|---|---|---|---|---|---|---|---|
| Hoisting | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
It means that:
All variables in JavaScript have hosting!
But how about example at the beginning, we saw that behavior was so different.
It's different, but it's not because of hosting, because of different variables Initialization behavior that I described above in the table.
To the question: "Do all variables have hoisting?"
The best answer is:
Yes, but with different initialization behaviors:
For
var, it is hoisted and initialized toundefinedduring the initial phase, with its value being assigned in the execution phase. Forletandconst, they are hoisted but not initialized until the execution phase, leading to the temporal dead zone, for others in the initial phase.
Precedence
The last thing I want to mention is that hoisting behaves differently for different types of variables.
function wrapperFn(test) {
console.log(typeof test === "function" ? test() : test);
function test() {
return 1;
}
var test = 2;
}
wrapperFn(0);
You will see 1 in the console because the last function declaration has the lowest hoisting priority and is declared last.
Happy coding! 👋