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

  1. Parsing: Source code → Abstract Syntax Tree (AST).
  2. Interpretation: AST → Bytecode (executed immediately).
  3. JIT Compilation: Frequently executed (“hot”) code → Machine code at runtime.
  4. 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 const or let, which has block scope.
  • arguments passed into the function(forms a argument object)

Scope chain

When you try to access a variable:

  1. The JS engine first looks for it in the current lexical environment.
  2. If not found, it goes to the outer environment (parent scope).
  3. 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/const rather than var when 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
2
3
4
5
6
7
8
9
public class person(String name){
int age = 20; //This is not the class attribute
this.name = name;
}

Person a = new Person(emulisy);

System.out.println(a.name); //we get emulisy
System.out.println(a.age); //error

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const emulisy = {
birthYear: 2003,
name: emulisy,
getAge() {
return 2025 - this.birthYear;
}
}
console.log(emulisy.getAge()); //this one would behave just like in java, and give 22.

const sophy = {
birthYear: 2004,
name: sophy,
}
sophy.getAge = emulisy.getAge;

console.log(sophy.getAge); //However, this gives the correct 21, but in java this would give 22.

This is because in Javascript, this is dynamic and its target is defined by its usage.

What this points to?

  • In the Global Scope this points to the window object
  • In a regular function call, this points to window by default, or to undefined in strict mode.
  • In a function call that has an owner, this points to the caller object.
  • Arrow Functions are not this binding, therefore the this inherits from its outer scope.
  • When using new to create an object with constructor, this points to the newly created object.

bind apply and call

There are ways to manually control this:

1
2
3
4
5
6
7
8
const person = {
name: Emulisy,
greet(year) {
console.log(`Greetings ${this.name}! Welcome to ${year}`);
}
}
person.greet(2025); //Greetings Emulisy! Welcome to 2025
const greetFunc = person.greet;

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
2
3
const sophia = {
name: Sophia,
}

Now I want to print Greetings Sophia! Welcome to 2025

  • call: the basic syntax is
    1
    greetFunc.call(sophia, 2025);
    The first argument is what this points to, and the rest argument is what passed to the original function.
  • apply: it’s almost the same as call
    1
    greetFunc.apply(sophia, [2025]);
    But we store all original arguments in an array.
  • bind: bind works differently.
    1
    2
    const greetSophia = greetFunc.bind(sophia, 2025);
    greetSophia;
    Doing so we permanently bind this in greetSophia to sophia and the argument is always 2025. Even if I use greetSophia(2026), the passed argument is still 2025.
    However, bind does not change the original function, it creates a new function that has specific this binding and argument(optional).

Functions Behind the scene

Function Lifecycle

  1. 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.

  2. 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.

  3. Execution:

    This is where the function actually executes line by line.

  4. 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function createCounter() {
let count = 0; // private variable
return {
increment() {
count++;
console.log(count);
},
reset() {
count = 0;
}
};
}

const counter = createCounter();
counter.increment(); // 1
counter.increment(); // 2
console.log(counter.count); // undefined

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 person is an object reference that points to the actual object in heap。

1
let emulisy = person;

Here we created another reference, but both emulisy and person points 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
    5
    a = 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.
      1
      2
      3
      4
      let obj1 = { name: "Sophia" };
      let obj2 = obj1;
      obj2.name = "Emulisy";
      console.log(obj1.name); // "Emulisy" — same reference
      But this is NOT a shallow copy, this is simply assigning a new reference since no object is created.

Shallow copy

A shallow copy creates a new object, but the inner objects are still shared (same reference).

1
2
3
4
5
6
7
8
9
10
11
12
const person = {
name: "Sophia",
address: { city: "Melbourne" }
};

const copy = { ...person }; // or Object.assign({}, person)
copy.name = "Emulisy";
copy.address.city = "Sydney";

console.log(person.name); // "Sophia"
console.log(person.address.city); // "Sydney" shared reference

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
    9
    const person = {
    name: "Sophia",
    address: { city: "Melbourne" }
    };

    const deepCopy = structuredClone(person);
    deepCopy.address.city = "Sydney";

    console.log(person.address.city); // "Melbourne"
  • Use Json: (usually limited)
    1
    const deepCopy = JSON.parse(JSON.stringify(person));
    This is not recommended as JSON cloning removes functions, undefined, and special types.
  • Clone manually using recursion: (nobody would do that lol)
    1
    2
    3
    4
    5
    6
    7
    8
    function 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;
    }