Javascript Engine
Javascript overview
- High-level: As a high level language, unlike C language, js arranges garbage collection and memory allocation for us.
- Just-in-time compiled:
- Multi-paradigm: JS supports procedural, object-oriented and functional programming, unlike C which does not support OOP(object-oriented programming), making JS more flexible and versatile.
- Single threaded with non-blocking event loop: enables concurrency, which is handling multiple tasks at the same time.
Compiled language vs Interpreted language vs Javascript
| Knowledge point | Compiled languages | Interpreted languages |
|---|---|---|
| Execution process | Compiled ahead-of-time into machine code. | Interpreted at runtime line by line, but still need to be compiled into machine code. |
| Output files | Produces an executable (.exe, .out). | Usually no standalone file; may produce bytecode (.pyc). |
JS on the other hand is not purely interpreted nor compiled:
Modern JavaScript Execution Process
- Parsing: Source code → Abstract Syntax Tree (AST).
- Interpretation: AST → Bytecode (executed immediately).
- JIT Compilation: Frequently executed (“hot”) code → Machine code at runtime.
- Optimization: Compiled machine code is cached for future use.
Key Differences
| Compiled Language | Interpreted Language | JavaScript (Modern) | |
|---|---|---|---|
| Translation time | Before execution | During execution | During execution (JIT) |
| Machine code output | Yes (permanent binary) | No | Yes (temporary, generated at runtime) |
| Performance | Fast | Slower | Fast after JIT optimization |
| Recompilation needed | Yes, if code changes | No | No, JIT dynamically adapts |
| Flexibility / Portability | Low (platform-dependent) | High (platform-independent) | Very high (runs on any JS engine) |
| Examples | C, C++, Rust | Python, Ruby, PHP | JavaScript (V8, SpiderMonkey, etc.) |
JS Engine and Runtime
JS Engine is a program that executes JS code, just like the V8 in Chrome, which supports JS and Node-JS.
All JS engines have two parts: a call stack and a heap.
- Heap: where the engine stores all the objects, it is an unstructured memory pool.
- Call Stack: contains execute contexts, and is where our code is executed.
JS Runtime
If we imagine JS runtime as a box, then in the box we have three components:
- JS Engine
- Web APIs: DOM, Fetch API, etc.
- Callback queue: where callback functions are stored
Together, they form what we call the Event Loop mechanism, especially between call stack in JS engine and the callback queue.
Execution context
What is Execution Context
We explained that inside the call stack in JS engine where the code is actually executed, there are execution contexts.
An execution context is an environment in which a piece of JavaScript code is evaluated and executed.
It defines how and where variables and functions are accessible, and what value this refers to.
There are usually two types of execution context:
Global Execution Context (GEC):
Created (only once) when the JavaScript program starts. It contains top level codes like global variables and functions (codes that are not in any function), and represents the global scope (e.g., window in browsers, global in Node.js).
Function Execution Context (FEC):
Created every time a function is called. Each function has its own context with new variable scope and this binding
What is inside Execution Context
When the execution context is created, JS sets up the environment and:
- Creates the Variable Environment
- Creates the Lexical Environment
- Sets the target of
this
When the execution context is in execution phase:
- The code is executed line by line.
- Variables are assigned their actual values.
- Functions are invoked and possibly create new execution contexts.
Variable Environment: contains:
- variables declared with
var- function declarations
and both of them are hoisted in the creation phase
Lexical Environment contains Environment Record + Reference to Outer Lexical Environment, that is:
- Reference to outer scopes (basis of scope chain)
- variable declared with
constorlet, which has block scope.- arguments passed into the function(forms a
argumentobject)
Scope chain
When you try to access a variable:
- The JS engine first looks for it in the current lexical environment.
- If not found, it goes to the outer environment (parent scope).
- This continues until:
- The variable is found, or
- The engine reaches the global scope (and throws an error if not found).
The basis is that the inner function stores the lexical environment of the outer function.
Hoisting and TDZ(Temporary Dead Zone)
In the creation phase of the execution context, all variables var let const are hoisted, meaning their declarations are moved to the top of their scope in memory.
| Declaration Type | Hoisted? | Initialized? | Accessible Before Declaration? |
|---|---|---|---|
var |
✅ Yes | ✅ Initialized to undefined |
✅ Yes (value = undefined) |
let |
✅ Yes | ❌ Not initialized | ❌ No (in TDZ) |
const |
✅ Yes | ❌ Not initialized | ❌ No (in TDZ) |
function |
✅ Yes | ✅ Fully initialized | ✅ Yes (can be called) |
TDZ: A region between entering the scope and the actual declaration of
let/const, where the variable cannot be accessed.Its purpose is to enforce “declare before use”, which increases code safety and clarity. Hence, we usually use
let/constrather thanvarwhen declaring variables.
This Keyword
Dynamic this
After learning Java, I always though this keyword is static and bind with the object when it is created:
1 | public class person(String name){ |
The name of the Person class is set using this by the time we create the a object with new keyword.
However, that is not the case in Javascript. this keyword is dynamic in js:
1 | const emulisy = { |
This is because in Javascript, this is dynamic and its target is defined by its usage.
What this points to?
- In the Global Scope
thispoints to thewindowobject - In a regular function call, this points to
windowby default, or toundefinedin strict mode. - In a function call that has an owner,
thispoints to the caller object. - Arrow Functions are not
thisbinding, therefore thethisinherits from its outer scope. - When using
newto create an object with constructor,thispoints to the newly created object.
bind apply and call
There are ways to manually control this:
1 | const person = { |
if we simply call greetFunc(2025), since it is a regular function call, this points to the undefined in strict mode.
Assuming I have another object:
1 | const sophia = { |
Now I want to print Greetings Sophia! Welcome to 2025
call: the basic syntax isThe first argument is what1
greetFunc.call(sophia, 2025);
thispoints to, and the rest argument is what passed to the original function.apply: it’s almost the same ascallBut we store all original arguments in an array.1
greetFunc.apply(sophia, [2025]);
bind: bind works differently.Doing so we permanently bind1
2const greetSophia = greetFunc.bind(sophia, 2025);
greetSophia;thisin greetSophia to sophia and the argument is always 2025. Even if I usegreetSophia(2026), the passed argument is still 2025.
However,binddoes not change the original function, it creates a new function that has specificthisbinding and argument(optional).
Functions Behind the scene
Function Lifecycle
Creation:
When the function is created in the compile phase, it will be stored in the heap as an object, containing its lexical environment and identifier. This is when the scope chain is created, as the engine now memorizes “where” the function is located.
Invocation:
When we invoke a function, a new execution context will be created and pushed into the stack, while the parameters and variables in it will be assigned specific value. Then the function will be ready for execution.
Execution:
This is where the function actually executes line by line.
Destruction:
After execution, the engine pops the execution context from the call stack, and the variable environment will be destroyed. If there is no closure, the local variables will be handled by GC(garbage collection).
Immediately Invoked Function Expression:
Functions that are invoked and executed instantly upon creation. The syntax is:
1
2
3
4
5
6
7 (function() {
console.log("IIFE runs!");
})();
//or
(() => {
console.log("arrow IIFE");
})();
Closure
A closure happens when the function’s outer environment (in heap) outlives its call stack frame.
When a function finished execution, its execution context will be destroyed. But its lexical environment will be passed to inner functions, and will be stored in the inner functions scope chain. Therefore, if we assign inner function to a variable, we can still access the outer variable after the outer function is gone.
Why do we need closure?
Closure is very useful in memorization, function factories and defining private variables. Here we confuse on the last one:
In Java, we can use the identifier private to make sure that the variable can only be accessed through its parent class. This enables encapsulation. But we don’t have this kind of identifiers in js.
1 | function createCounter() { |
By storing the returned function, we created a closure. This way we can only access the count using the inner function.
Memory Management and Garbage Collection
Memory allocation
Memory is allocated whenever you create data:
- For primitive types: stored in call stack
- For objects: stored in heap
- For object references: also stored in the stack
Object References:
1
2
3
4 const person = {
name: Emulisy,
Gender: male
}Here the
personis an object reference that points to the actual object in heap。
1 let emulisy = person;Here we created another reference, but both
emulisyandpersonpoints to the same object.
Shallow and deep copy
- Primitive Value:
Primitive values are stored in the stack, and are copied by value.1
2
3
4
5a = 10;
b = a;
a = 5;
console.log(a); //5
console.log(b); //10- Objects:
Objects are stored in the heap, and we can only access them using object references. So when we copy the object references, we create a new pointer to the same object.But this is NOT a shallow copy, this is simply assigning a new reference since no object is created.1
2
3
4let obj1 = { name: "Sophia" };
let obj2 = obj1;
obj2.name = "Emulisy";
console.log(obj1.name); // "Emulisy" — same reference
- Objects:
Shallow copy
A shallow copy creates a new object, but the inner objects are still shared (same reference).
1 | const person = { |
We can see that although the copy is a different object, its inner object address is the same as the person.
So how to create a shallow copy:
- Use the spread operator:
newObj = {...oldObj} - Use
Object.assign({}, oldObj)
Object.assign(target, ...source): put enumerable properties in the source to the target, doing so changes the target.But we can also create a new object using
const newObj = Object.assign({}, oldObj)We can also copy multiple objects into the new object:
1
2
3
4
5
6 const obj1 = { a: 1 };
const obj2 = { b: 2 };
const obj3 = { c: 3 };
const result = Object.assign({}, obj1, obj2, obj3);
console.log(result); // { a: 1, b: 2, c: 3 }
Deep Clone
A deep copy duplicates all levels of nested objects, so nothing is shared.
To create a deep clone, we can:
- Using
structuredClone(most recommended)1
2
3
4
5
6
7
8
9const person = {
name: "Sophia",
address: { city: "Melbourne" }
};
const deepCopy = structuredClone(person);
deepCopy.address.city = "Sydney";
console.log(person.address.city); // "Melbourne" - Use Json: (usually limited) This is not recommended as JSON cloning removes functions, undefined, and special types.
1
const deepCopy = JSON.parse(JSON.stringify(person));
- Clone manually using recursion: (nobody would do that lol)
1
2
3
4
5
6
7
8function deepClone(obj) {
if (obj === null || typeof obj !== "object") return obj;
const clone = Array.isArray(obj) ? [] : {};
for (let key in obj) {
clone[key] = deepClone(obj[key]);
}
return clone;
}

