Javascript Execution Context and Scope
Preface
When I first started learning Javascript, it was very easy to write code like:
test();
const test = ()=>{
console.log('demo')
}
And only after seeing the undefined error at runtime did I learn to swap the order of declaration and execution; but declaring a function with function does not have this problem:
test();
function test(){
console.log('demo')
}
There are also the variable declaration keywords var, let, and const. After stepping into a few traps, I roughly remembered when to use which one — bits of fragmented experience like const is for declaring immutable constants, let is for declaring mutable variables, var can be declared casually but be careful in for loops, and so on.
But after learning more about how the browser, Node, and V8 work, I came to understand the basic principles behind those fragmented experiences.
Hoisting
Hoisting means that during the execution of Javascript code, the Javascript engine moves the variable declarations and function declarations to the top of the Javascript code, with a default value of undefined.
For example:
const data = 1;
What actually happens during compilation and execution is more like:
const data = undefined;
data = 1;
Javascript handles variable declaration and assignment in different stages. During the compilation stage, Javascript declares the variable in the execution context and gives it a value of undefined; during the execution stage, the value is then actually assigned.
When a function is declared with the function keyword, during Javascript’s compilation stage, the function’s content is stored in the Heap, and a property with the function’s name is added to the environment object, whose value is a pointer to the address of the function’s content in the Heap.
For example, after declaring a function sayHello in the Chrome console, you can access this function on the window object:

Therefore, when using function to declare a function, you can call it before declaring it, because at compile time Javascript directly adds the property on the environment object; whereas if you use other keywords to assign a function’s content, you must declare it before calling it, otherwise it will be undefined.
Scope
Before ES6, Javascript only had global scope and function scope, and the only keyword for declaring variables was var. Variables and objects declared in the global scope can be accessed from anywhere; variables or functions defined inside a function (function scope) can only be accessed inside the function and are destroyed once the function finishes executing.
With only these conditions, there is a big drawback: because of Hoisting, variables in the global scope can be accessed from anywhere, and you do not know when they might be modified, overridden by variables of the same name, or even when variables that should be destroyed are not destroyed.
A very common interview question:
function foo(){
for (var i = 0; i < 7; i++) {
}
console.log(i);
}
foo()
Because var i is hoisted, after the for loop ends, the variable i can still be accessed inside the function.
Therefore, after ES6, let and const were introduced, both supporting block scope. When variables are declared with let or const, they are not hoisted; instead, they are stored in a region of the lexical environment within the execution context.
The lexical environment maintains a stack structure. When a block scope declares a variable with let/const, such as for(let i=0;i<10;i++){...}, the Javascript engine pushes the variable onto the stack in the lexical environment; when the for loop ends, the variable is popped off, achieving the goal of destroying the variable when the block finishes executing.
ChangeLog
- 20221103 - init
- 20260501–translate by claude code