A small and isolated environment where a specific piece of code is interpreted and converted into machine language.
JavaScript is an interpreted language. To understand this better, let’s look at interpreters, compilers, and JIT (Just-In-Time) compilers:
Interpreter: An interpreter runs instructions directly from the programming language without changing them into machine code first.
Easy debug, Slow Execution
Compiler: A compiler changes the entire program into object code (or binary code) and saves it. This code can then be run by the machine.
Faster Execution, System Crashes
JIT Compiler: A JIT compiler converts code into byte code first. Then, at runtime, it changes the byte code into machine-readable code, which makes the program run faster.
Interpretation + Compilation = JIT Compiler
JavaScript is mainly interpreted, but modern JavaScript engines, like V8 in Google Chrome, use JIT (Just-In-Time) compilation to boost performance. They convert JavaScript code into optimized machine code right before it runs. This mix of interpretation and JIT compilation makes JavaScript fast and versatile for web applications.
There are two types of execution context
Global execution context:
Created once, when the JS engine first starts running the script. It contains four main components — window, this, variable object, scope chain. Only one global execution context can exist per program.
Function execution context:
Created every time a function is called (a new one is created per invocation, even for recursive calls to the same function). It has its own arguments object, local variables, and scope chain that points back to the environment in which the function was defined.
Every execution context (global or function) goes through two phases:
Creation phase (a.k.a memory creation phase):
this keyword is determined.var variables are hoisted and initialized with undefined.let/const variables are hoisted but left uninitialized (Temporal Dead Zone).Execution phase:
console.log(a); // undefined (hoisted, not yet assigned)
console.log(greet()); // works, function is hoisted with its full body
var a = 10;
function greet() {
return "hi";
}
The call stack is where JavaScript keeps track of execution contexts — it’s a LIFO (Last In, First Out) structure. The global execution context sits at the bottom, and each function call pushes a new function execution context on top. When a function returns, its context is popped off the stack.
function multiply(a, b) {
return a * b;
}
function square(n) {
return multiply(n, n);
}
function printSquare(n) {
console.log(square(n));
}
printSquare(5);
// Call stack grows like this:
// printSquare(5)
// square(5)
// multiply(5, 5)
// Each function pops off the stack once it returns a value,
// unwinding back down to the global execution context.
If the stack grows without bound (e.g. a recursive function with no base case), JavaScript throws a RangeError: Maximum call stack size exceeded.
function recurse() {
return recurse();
}
recurse();
// RangeError: Maximum call stack size exceeded