JavaScript is deceptively simple to start with but has deep, powerful internals. Mastering these internals is what separates engineers who write code that works from those who write code that performs, scales, and is maintainable. This guide covers the concepts that power real-world JavaScript applications.


Step 1 — Execution Context & the Call Stack

Every time JavaScript runs code, it creates an Execution Context. Understanding this is the foundation for understanding closures, hoisting, and this.

execution-context.jsjavascript
// When JS engine starts, it creates the GLOBAL Execution Context
// It sets up: global object (window/global), 'this', outer scope

// HOISTING: declarations are processed before code runs
console.log(name); // undefined (var is hoisted, not the value)
var name = 'Kuldeep';
console.log(name); // 'Kuldeep'

// let/const are hoisted too but NOT initialized (Temporal Dead Zone)
console.log(age); // ReferenceError: Cannot access 'age' before initialization
let age = 21;

// Function declarations are FULLY hoisted (name + body)
greet(); // Works! 'Hello'
function greet() { console.log('Hello'); }

// Function expressions are NOT fully hoisted
sayBye(); // TypeError: sayBye is not a function
var sayBye = function() { console.log('Bye'); };

// THE CALL STACK
function third() { console.log('Third'); }
function second() { third(); }
function first() { second(); }
first();
// Call stack: [global] -> [first] -> [second] -> [third] -> pops back

Step 2 — Scope, Closures & the Scope Chain

A closure is a function that remembers the variables from its outer scope even after the outer function has finished executing. It's one of the most powerful and commonly used patterns in JavaScript.

closures.jsjavascript
// Closure: inner function closes over outer variables
function makeCounter(start = 0) {
  let count = start; // 'count' is in makeCounter's scope

  return {
    increment() { return ++count; },
    decrement() { return --count; },
    reset()     { count = start; return count; },
    value()     { return count; }
  };
}

const counter = makeCounter(10);
console.log(counter.increment()); // 11
console.log(counter.increment()); // 12
console.log(counter.decrement()); // 11
console.log(counter.value());     // 11
// 'count' is private — can't access it from outside

// PRACTICAL: Data privacy with closures
function createUser(name, role) {
  // Private variables
  let _name = name;
  let _role = role;
  let _loginCount = 0;

  return {
    login() { _loginCount++; console.log(`${_name} logged in`); },
    getInfo() { return { name: _name, role: _role, logins: _loginCount }; },
    // Prevents external mutation of name
  };
}

const user = createUser('Kuldeep', 'admin');
user.login(); // 'Kuldeep logged in'
user.getInfo(); // { name: 'Kuldeep', role: 'admin', logins: 1 }

// CLOSURE IN LOOPS (classic gotcha)
// BAD — all timeouts share the same 'i' variable
for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100); // 3, 3, 3
}

// GOOD — use let (block-scoped, creates new binding per iteration)
for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100); // 0, 1, 2
}

Step 3 — Prototypes & the Prototype Chain

JavaScript is a prototype-based language. Every object has a hidden [[Prototype]] link to another object. When you access a property that doesn't exist on an object, JS walks up the chain.

prototypes.jsjavascript
// Every object has a prototype
const dog = { name: 'Rex', sound: 'Woof' };
console.log(Object.getPrototypeOf(dog) === Object.prototype); // true

// Object.create() sets the prototype explicitly
const animal = {
  speak() { console.log(`${this.name} says ${this.sound}`); }
};
const cat = Object.create(animal);
cat.name = 'Whiskers';
cat.sound = 'Meow';
cat.speak(); // 'Whiskers says Meow' — 'speak' found via prototype chain

// Constructor function pattern
function Person(name, age) {
  this.name = name;
  this.age = age;
}
// Methods go on prototype (shared across all instances, not copied)
Person.prototype.greet = function() {
  return `Hi, I'm ${this.name}`;
};
const p1 = new Person('Alice', 25);
const p2 = new Person('Bob', 30);
console.log(p1.greet()); // 'Hi, I'm Alice'
console.log(p1.greet === p2.greet); // true — same function reference!

// ES6 Class: SYNTACTIC SUGAR over prototype chain
class Student extends Person {
  constructor(name, age, gpa) {
    super(name, age); // calls Person constructor
    this.gpa = gpa;
  }
  study() {
    return `${this.name} is studying. GPA: ${this.gpa}`;
  }
  // Override parent method
  greet() {
    return `${super.greet()}, I'm a student!`;
  }
}

const s = new Student('Carol', 20, 9.2);
console.log(s.greet()); // 'Hi, I'm Carol, I'm a student!'
console.log(s instanceof Student); // true
console.log(s instanceof Person);  // true — prototype chain!

Step 4 — The Event Loop in Depth

JavaScript is single-threaded but can handle async operations via the event loop. Understanding its queues is essential for predicting execution order.

event-loop.jsjavascript
// Execution order: synchronous > microtasks > macrotasks
console.log('1: Script start'); // synchronous

setTimeout(() => console.log('5: setTimeout'), 0); // macrotask

Promise.resolve()
  .then(() => console.log('3: Promise 1'))  // microtask
  .then(() => console.log('4: Promise 2')); // microtask (chained)

console.log('2: Script end'); // synchronous

// Output: 1, 2, 3, 4, 5
// Explanation:
// 1. Synchronous code runs first: '1', '2'
// 2. Microtask queue drains completely: '3', '4'
// 3. Macrotask queue runs one task: '5'

// queueMicrotask() adds to microtask queue
console.log('A');
queueMicrotask(() => console.log('C')); // microtask
console.log('B');
// Output: A, B, C

// requestAnimationFrame — fires before the next paint
requestAnimationFrame(() => {
  // Runs before browser paints the frame
  element.style.transform = 'translateX(10px)';
});

Queue Priority Order

  • 1. Synchronous code (call stack) — runs first, always.
  • 2. Microtask queue — Promise callbacks, queueMicrotask, MutationObserver. Drains COMPLETELY before macrotasks.
  • 3. Rendering step — browser repaints the screen.
  • 4. Macrotask queue — setTimeout, setInterval, I/O, UI events. ONE task runs, then back to microtasks.

Step 5 — Promises Deep Dive

promises.jsjavascript
// A Promise has 3 states: pending, fulfilled, rejected
const p = new Promise((resolve, reject) => {
  const success = Math.random() > 0.5;
  if (success) resolve({ data: 'Users list' });
  else reject(new Error('Server error'));
});

p.then(result => console.log(result))
 .catch(err => console.error(err))
 .finally(() => console.log('Cleanup')); // always runs

// CHAINING — each .then returns a new Promise
fetch('/api/user/1')
  .then(res => {
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return res.json();     // returns a Promise
  })
  .then(user => fetch(`/api/posts?userId=${user.id}`))
  .then(res => res.json())
  .then(posts => console.log(posts))
  .catch(err => console.error('Failed:', err));

// Promise.all — waits for ALL, rejects if ANY fails
const [user, posts, comments] = await Promise.all([
  fetch('/api/user').then(r => r.json()),
  fetch('/api/posts').then(r => r.json()),
  fetch('/api/comments').then(r => r.json()),
]);

// Promise.allSettled — waits for ALL, never rejects
const results = await Promise.allSettled([p1, p2, p3]);
results.forEach(result => {
  if (result.status === 'fulfilled') console.log(result.value);
  else console.error(result.reason);
});

// Promise.race — first one to settle wins
const fastest = await Promise.race([
  fetch('/api/primary'),
  fetch('/api/backup'),
]);

// Promise.any — first FULFILLED wins (ignores rejections)
const firstSuccess = await Promise.any([p1, p2, p3]);

Step 6 — Async/Await

async-await.jsjavascript
// async functions always return a Promise
async function fetchUser(id) {
  // await pauses the function until the Promise resolves
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json(); // auto-wrapped in Promise.resolve()
}

// Error handling with try/catch
async function loadDashboard(userId) {
  try {
    const user = await fetchUser(userId);
    const posts = await fetch(`/api/posts?user=${user.id}`).then(r => r.json());
    return { user, posts };
  } catch (err) {
    console.error('Dashboard load failed:', err);
    return null;
  } finally {
    hideLoadingSpinner();
  }
}

// SEQUENTIAL vs PARALLEL — common performance mistake
// BAD: sequential (takes 200ms + 200ms = 400ms)
async function sequential() {
  const user = await fetchUser(1);   // waits 200ms
  const post = await fetchPost(1);   // THEN waits 200ms
  return { user, post };
}

// GOOD: parallel (takes only ~200ms total)
async function parallel() {
  const [user, post] = await Promise.all([
    fetchUser(1),
    fetchPost(1),  // both start at the same time!
  ]);
  return { user, post };
}

// For loops with async/await
async function processAll(ids) {
  // Sequential (each waits for previous):
  for (const id of ids) {
    await processItem(id);
  }

  // Parallel (all at once):
  await Promise.all(ids.map(id => processItem(id)));

  // Parallel with concurrency limit (3 at a time):
  const limit = 3;
  for (let i = 0; i < ids.length; i += limit) {
    const batch = ids.slice(i, i + limit);
    await Promise.all(batch.map(id => processItem(id)));
  }
}

Step 7 — Generators & Iterators

generators.jsjavascript
// Generator: a function that can pause and resume
function* counter(start = 0) {
  while (true) {
    yield start++; // pauses here, returns value
  }
}

const gen = counter(5);
console.log(gen.next()); // { value: 5, done: false }
console.log(gen.next()); // { value: 6, done: false }

// Finite generator
function* range(start, end, step = 1) {
  for (let i = start; i < end; i += step) {
    yield i;
  }
}

for (const n of range(0, 10, 2)) {
  console.log(n); // 0, 2, 4, 6, 8
}

// Making any object iterable with Symbol.iterator
class Fibonacci {
  constructor(limit) { this.limit = limit; }

  [Symbol.iterator]() {
    let a = 0, b = 1, count = 0;
    const limit = this.limit;
    return {
      next() {
        if (count++ >= limit) return { done: true };
        const value = a;
        [a, b] = [b, a + b];
        return { value, done: false };
      }
    };
  }
}

const fib = new Fibonacci(8);
console.log([...fib]); // [0, 1, 1, 2, 3, 5, 8, 13]

Step 8 — Functional Programming Patterns

functional.jsjavascript
const students = [
  { name: 'Alice', grade: 92, subject: 'Math' },
  { name: 'Bob',   grade: 78, subject: 'Math' },
  { name: 'Carol', grade: 88, subject: 'Physics' },
  { name: 'Dave',  grade: 95, subject: 'Physics' },
];

// map: transform each element
const names = students.map(s => s.name);
// ['Alice', 'Bob', 'Carol', 'Dave']

// filter: keep elements matching predicate
const topStudents = students.filter(s => s.grade >= 90);
// [Alice (92), Dave (95)]

// reduce: fold array into a single value
const totalGrade = students.reduce((sum, s) => sum + s.grade, 0);
const average = totalGrade / students.length;

// Chaining (functional pipeline)
const mathTopStudents = students
  .filter(s => s.subject === 'Math')
  .map(s => ({ ...s, letterGrade: s.grade >= 90 ? 'A' : 'B' }))
  .sort((a, b) => b.grade - a.grade);

// CURRYING: transform a multi-arg function into a chain of single-arg functions
const multiply = a => b => a * b;
const double = multiply(2);
const triple = multiply(3);
console.log(double(5)); // 10
console.log(triple(5)); // 15

// COMPOSE: right-to-left function composition
const compose = (...fns) => x => fns.reduceRight((acc, fn) => fn(acc), x);
const trim = s => s.trim();
const toLower = s => s.toLowerCase();
const normalize = compose(toLower, trim);
console.log(normalize('  HELLO WORLD  ')); // 'hello world'

Step 9 — Performance Patterns

performance.jsjavascript
// DEBOUNCE: delay execution until after 'delay' ms of inactivity
function debounce(fn, delay) {
  let timer;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

// THROTTLE: execute at most once every 'interval' ms
function throttle(fn, interval) {
  let lastTime = 0;
  return function(...args) {
    const now = Date.now();
    if (now - lastTime >= interval) {
      lastTime = now;
      fn.apply(this, args);
    }
  };
}

// Usage
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce(handleSearch, 300));
window.addEventListener('scroll', throttle(handleScroll, 100));

// MEMOIZATION: cache results of expensive pure functions
function memoize(fn) {
  const cache = new Map();
  return function(...args) {
    const key = JSON.stringify(args);
    if (cache.has(key)) return cache.get(key);
    const result = fn.apply(this, args);
    cache.set(key, result);
    return result;
  };
}

const expensiveFib = memoize(function fib(n) {
  if (n <= 1) return n;
  return expensiveFib(n - 1) + expensiveFib(n - 2);
});

console.time('first call');
expensiveFib(40); // takes some ms
console.timeEnd('first call');

console.time('second call');
expensiveFib(40); // near-instant from cache
console.timeEnd('second call');