JavaScript Core Mastery · Final Recap · 30 min read

Core JavaScript: The Last-Minute Recap

All 14 lessons, compressed into one read for the morning of an interview — concept, a runnable example with its real output, and a ready-made spoken answer for each, plus an output-prediction drill and rapid-fire Q&A.

How to use this page: each section is concept → example → what to actually say out loud. Don't skim the examples — read the output comment and make sure you would have predicted it. If you wouldn't have, that's the one lesson to re-open before you close your laptop.

Jump to

  1. Key words in plain English
  2. 1. Execution Context
  3. 2. Call Stack
  4. 3. var, let, const
  5. 4. Variable Environments
  6. 5. Window & Global this
  7. 6. undefined vs. Not Defined
  8. 7. Scope Chain
  9. 8. Temporal Dead Zone
  10. 9. Block Scope & Shadowing
  11. 10. Closures
  12. 11. setTimeout + Closures
  13. 12. Callbacks & Listeners
  14. 13. JS Engine & V8
  15. 14. Event Loop
  16. Output-prediction drill
  17. Rapid-fire Q&A

Key Words In Plain English

  • Execution context is the engine's working area for one piece of running code. The first one is the Global Execution Context (GEC). Every function call makes a new one.
  • Hoisting is the engine reserving memory for declarations before any line runs. It does not move your code.
  • Call stack is the list of calls running right now. It is LIFO (last in, first out): the newest call finishes first.
  • Recursion is a function calling itself. A base case is the condition that makes it stop.
  • Scope is the part of the code where a name can be used. A block is code inside curly braces. Binding is the link between a name and its value.
  • Temporal Dead Zone (TDZ) is the time between a let/const name being reserved and its declaration line running. Using the name then throws an error.
  • Mutation is changing a value in place, such as obj.x = 1. Reassignment is pointing a name at a new value, such as x = 2.
  • Global object is the top-level object of the environment: window in a browser. globalThis is the same thing under a name that works everywhere. Strict mode is an opt-in mode ("use strict") with stricter rules, for example a plain function call has this as undefined.
  • undefined is the value of a variable that exists but has no value yet. null is a value that you set on purpose to mean “empty”. Not defined is the error for a name that was never declared. typeof returns the type of a value as a string.
  • Lexical Environment is the engine's record of one scope's variables plus a link to the outer scope. Variable Environment is the record of the variables made by one function call. Scope chain is the chain of those links, from inner to Global. Lexical (static) scoping means the chain follows where the code is written, not where it is called.
  • Shadowing is when an inner scope declares a name that an outer scope already has, so the inner one hides the outer one.
  • Closure is a function that keeps access to the variables of the place where it was created, even after that place has finished.
  • Garbage collection is the engine automatically freeing memory that nothing can reach any more.
  • IIFE (Immediately Invoked Function Expression) is a function that is defined and called in one step. .bind() returns a copy of a function with some arguments already filled in.
  • Callback is a function you give to other code so that it can call it later. Higher-order function is a function that takes or returns a function. Event listener is a callback that runs when an event, such as a click, happens.
  • JavaScript engine is the program that runs JavaScript (V8 in Chrome and Node.js). Runtime environment is the engine plus the extras of its host, such as timers and the event loop.
  • Parsing turns source text into tokens and then an AST (a tree of the code's structure). Bytecode is compact instructions for a simple virtual machine. Machine code is what the CPU runs directly. JIT (Just-In-Time) compilation means compiling the code that runs often while the program is running.
  • Web API is a feature provided by the browser, not by the language, such as setTimeout and fetch.
  • Event loop is the loop that moves waiting callbacks onto the call stack when it is empty. Macrotask (callback) queue holds timer and event callbacks. Microtask queue holds promise callbacks and is emptied first. Promise is an object for a result that is not ready yet. Starvation is when something never gets a turn because other work keeps going first.

1Execution Context

  • Before a single line runs, JS creates the Global Execution Context (GEC).
  • Every context is built in two phases: the memory creation phase scans the whole scope and allocates memory upfront, then the code execution phase runs line by line.
  • In the memory phase, var gets undefined and a function declaration gets its entire body. That's all hoisting is.
execution-context.js
console.log(count);     // undefined  ← memory phase already created it
console.log(increment); // ƒ increment() { ... }  ← whole function is ready

var count = 0;
function increment() {
  count++;
}

increment();
console.log(count);     // 1  ← now the code phase has run
Say it like this

“JavaScript creates an execution context in two phases. In the memory phase it allocates space for every variable and function — vars get undefined, function declarations get their full definition. Then the execution phase runs the code line by line. That's why I can log a var before its line and get undefined instead of an error.”

2Call Stack

  • The call stack tracks which execution context is running right now. LIFO — last in, first out.
  • Each call pushes a context; each return pops it off, handing control back to whatever was underneath.
  • No base case in a recursive function → the stack fills → RangeError: Maximum call stack size exceeded.
call-stack.js
let count = 0;

function bump() {
  count++;
  console.log("bump →", count);
}

function runTwice() {
  bump();
  bump();
  console.log("runTwice done");
}

runTwice();
// bump → 1
// bump → 2
// runTwice done

// Stack over time:
// [GEC] → [GEC, runTwice] → [GEC, runTwice, bump] → [GEC, runTwice]
//       → [GEC, runTwice, bump] → [GEC, runTwice] → [GEC]
Say it like this

“The call stack is how JS keeps track of where it is. Calling a function pushes a new execution context on top; returning pops it off. Since there's only one stack, only one thing runs at a time — and infinite recursion overflows it.”

3var, let, const

  • Scope: var is function/global, let and const are block.
  • All three are hoisted — var to undefined, let/const into the TDZ.
  • var can be redeclared; let/const throw a SyntaxError on redeclaration.
  • const locks the binding, not the value — mutation is still allowed.
declarations.js
var count = 0;
count++;              // fine
var count = 10;       // fine — var can be redeclared

let total = 0;
total++;              // fine — reassigning is allowed
// let total = 5;     // SyntaxError: Identifier 'total' has already been declared

const LIMIT = 5;
// LIMIT++;           // TypeError: Assignment to constant variable

const counter = { count: 0 };
counter.count++;      // ALLOWED — mutating the object's contents
console.log(counter); // { count: 1 }
// counter = {};      // TypeError — reassigning the binding is not allowed
Say it like this

“const freezes the binding, not the value. I can't point the variable at something new, but if it holds an object I can still change what's inside it — which is why counter.count++ works on a const object.”

4Functions & Variable Environments

  • Every function call gets its own private Variable Environment — its own fresh copy of parameters and local variables.
  • Two calls to the same function never share local state. That environment dies when the call ends — unless a closure keeps it alive (section 10).
  • A function can read outward into its parent's scope; the parent can never read inward.
variable-environment.js
function counter() {
  let count = 0;   // brand new variable on EVERY call
  count++;
  return count;
}

console.log(counter()); // 1
console.log(counter()); // 1  ← NOT 2 — fresh environment each time
console.log(counter()); // 1

Hold this example in your head — section 10 uses the exact same counter and gets 1, 2, 3 instead. The only difference is a closure. That contrast is the cleanest way to prove you understand both.

Say it like this

“Each function call creates its own execution context with its own variable environment, so local variables are completely isolated per call. That's why calling this counter three times gives me 1, 1, 1 — every call starts from a fresh count.”

5Window & Global this

  • In a browser the global object is window; globalThis is the portable name that works in Node too.
  • At the top level (non-strict), this === window.
  • Global var and function declarations attach to window; let/const do not.
global-this.js
var count = 0;
let total = 0;

console.log(window.count);    // 0          ← var attaches to window
console.log(window.total);    // undefined  ← let does not
console.log(this === window); // true       ← at top level, non-strict

function bump() {
  console.log(this === window); // true in non-strict, undefined-this in strict mode
}
bump();
Say it like this

“At global scope this is the window object. var declarations become properties of window, but let and const are stored in a separate declarative record, so you won't find them on window even though they're global.”

6undefined vs. Not Defined

  • undefined — the variable exists in memory, it just hasn't been given a value yet.
  • not defined — the variable was never declared anywhere; accessing it throws a ReferenceError.
  • typeof is the one safe existence check — it never throws on an undeclared name.
undefined-vs-not-defined.js
var count;
console.log(count);          // undefined      ← declared, no value yet
console.log(typeof count);   // "undefined"

console.log(total);          // ReferenceError: total is not defined
console.log(typeof total);   // "undefined"    ← safe, doesn't throw

let score = null;            // deliberately empty — different intent
console.log(score);          // null
console.log(typeof score);   // "object"  ← famous JS quirk, worth knowing
Say it like this

“undefined means the variable exists but hasn't been assigned yet — that's the placeholder JS puts there during the memory phase. not defined means it was never declared at all, which is a ReferenceError. They're completely different situations.”

7Scope Chain & Lexical Environment

  • Every execution context has a Lexical Environment = its own local memory + a reference to its parent's lexical environment.
  • Variable lookup walks outward only: local → parent → grandparent → global → else ReferenceError.
  • It's lexical/static: determined by where the function is written, never by where it's called.
scope-chain.js
let count = 100;         // global

function outer() {
  let step = 5;          // outer's scope

  function inner() {
    count += step;       // both found by walking outward
    console.log(count);
  }

  inner();
}

outer();                 // 105
console.log(step);       // ReferenceError — lookup never goes inward
Say it like this

“When a variable isn't found locally, JS follows the lexical environment's outer reference up the scope chain until it finds it or hits global. It's decided at author time by where the function sits in the code, not at call time.”

8Temporal Dead Zone (TDZ)

  • The TDZ is the gap between the start of a scope and the line where a let/const is declared.
  • The variable is hoisted — it exists — but it's uninitialized and untouchable until its line runs.
  • That error is distinct from "not defined" and interviewers listen for the difference.
tdz.js
// ---- TDZ starts here for `count` ----
console.log(total);   // undefined  ← var: hoisted AND initialized
console.log(count);   // ReferenceError: Cannot access 'count' before initialization

var total = 0;
let count = 0;
// ---- TDZ for `count` ends on the line above ----

count++;
console.log(count);   // 1
Say it like this

“let and const are hoisted — people often say they aren't, which isn't accurate. They're hoisted into a separate memory space but left uninitialized, so accessing them before their declaration throws ‘cannot access before initialization’ rather than giving undefined.”

9Block Scope & Shadowing

  • Any { } is a block — an if, a loop, or a bare block on its own.
  • let/const are contained by it; var walks straight through it and attaches to the nearest function/global scope.
  • Shadowing a let with an inner let creates a genuinely separate variable. Doing it with var isn't shadowing at all — it's the same variable being overwritten.
  • Shadowing an outer let with an inner var → SyntaxError (illegal shadowing).
block-scope.js
let count = 1;
{
  let count = 99;      // a separate variable, living only in this block
  count++;
  console.log(count);  // 100
}
console.log(count);    // 1   ← outer one never touched

var total = 1;
{
  var total = 99;      // NOT a copy — the exact same variable
  total++;
}
console.log(total);    // 100 ← the outer value was overwritten
Say it like this

“var isn't block scoped, so re-declaring it inside a block doesn't shadow anything — it is the same variable, and mutating it leaks out. With let the inner one is a separate variable that disappears when the block ends.”

10Closures

  • A closure = a function + a live reference to the lexical environment it was created in. Not a copy — a reference.
  • It's the exception to "popped context = memory freed": if a returned function still points into that environment, it stays alive in memory.
  • Each call to the outer function creates a separate, independent closure.
closures.js
function createCounter() {
  let count = 0;             // lives in createCounter's lexical environment

  return function () {
    count++;                 // still reachable after createCounter() has returned
    return count;
  };
}

const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2   ← count SURVIVED — this is the closure
console.log(counter()); // 3

const another = createCounter();
console.log(another()); // 1   ← a completely independent closure
console.log(counter()); // 4   ← the first one is untouched

The money comparison: section 4's counter printed 1, 1, 1 because each call made a fresh count. This one prints 1, 2, 3 because the returned function holds onto one single count that never gets garbage collected. Saying that out loud proves you understand execution contexts and closures in one sentence.

Say it like this

“A closure is a function bundled with a live reference to its outer scope. When createCounter returns, its execution context is popped off the stack, but because the returned function still references count, that memory isn't garbage collected — so the count keeps incrementing. And calling createCounter again gives a totally separate counter.”

11setTimeout + Closures (The Classic)

  • setTimeout never pauses the loop — it hands the callback off and the loop finishes immediately.
  • With var there is one shared variable; all callbacks read its final value. With let each iteration gets its own binding.
  • Four valid fixes: let, an IIFE, a helper function, or .bind().
settimeout-closures.js
// BROKEN — one shared `count`
for (var count = 1; count <= 3; count++) {
  setTimeout(() => console.log(count), count * 1000);
}
// 4, 4, 4   ← loop finished long before any callback ran

// FIX 1 — let gives each iteration its own binding
for (let count = 1; count <= 3; count++) {
  setTimeout(() => console.log(count), count * 1000);
}
// 1, 2, 3

// FIX 2 — IIFE creates a new scope per iteration (the pre-ES6 answer)
for (var count = 1; count <= 3; count++) {
  (function (captured) {
    setTimeout(() => console.log(captured), captured * 1000);
  })(count);
}
// 1, 2, 3
Say it like this

“With var there's only one variable for the whole loop, and all three callbacks close over that same one — by the time they run, it's already 4. let creates a fresh binding per iteration so each callback captures its own value. If I'm told to keep var, I'd wrap the body in an IIFE that takes the current value as a parameter, which creates a new scope each iteration.”

12First-Class Functions, Callbacks & Listeners

  • Functions are values — assignable, storable, passable, returnable.
  • A higher-order function takes and/or returns a function. A callback is a function handed off to be run later — synchronously or asynchronously.
  • removeEventListener needs the identical reference, not identical-looking code.
  • In a listener: a regular function's this is the element; an arrow function's this comes from the surrounding scope.
callbacks-listeners.js
let count = 0;

function bumpAndLog(step) {
  count += step;
  console.log(count);
}

// higher-order function: takes a callback, decides when to call it
function repeat(times, callback) {
  for (let i = 1; i <= times; i++) callback(i);
}

repeat(3, bumpAndLog);
// 1
// 3   (1 + 2)
// 6   (3 + 3)

// Listener pitfall
const btn = document.querySelector("#bump");
btn.addEventListener("click", bumpAndLog);
btn.removeEventListener("click", bumpAndLog);   // works — same reference
btn.removeEventListener("click", (n) => count++); // does nothing — new function
Say it like this

“Functions are first-class values in JS, so I can pass one into another function and let it decide when to invoke it — that's a callback, and addEventListener is just a higher-order function doing exactly that. The common bug is trying to remove a listener with a new inline arrow function — it has to be the same reference.”

13JS Engine & V8

  • Three steps: parse (source → tokens → AST), compile, execute.
  • JS is JIT-compiled: Ignition (interpreter) starts running bytecode instantly while TurboFan (optimizing compiler) compiles hot functions into fast machine code in the background — at the same time, not one after the other.
  • Execution uses the call stack + memory heap; Orinoco is V8's garbage collector, built on Mark-and-Sweep.
  • The engine is only the parser, compiler, stack and heap. Web APIs, the queues and the event loop belong to the runtime environment around it.
engine-v8.js
function increment(n) {
  return n + 1;
}

let count = 0;
for (let i = 0; i < 1_000_000; i++) {
  count = increment(count);
}
console.log(count); // 1000000

// What V8 did:
// 1. Parse    → tokens → AST
// 2. Ignition → bytecode, runs immediately
// 3. TurboFan → notices increment() is "hot" after many calls,
//               compiles it to optimized machine code mid-run
// 4. Orinoco  → frees memory nothing can reach anymore (Mark-and-Sweep)
Say it like this

“V8 parses the code into an AST, then Ignition turns that into bytecode and starts executing right away. Meanwhile TurboFan watches for hot functions and compiles those to optimized machine code in the background — that's JIT, getting an interpreter's fast startup plus a compiler's fast execution. And setTimeout isn't part of the engine at all; it's a browser API.”

14Async JS & The Event Loop

  • One call stack, one thing at a time. Slow work is handed off to Web APIs, outside the engine.
  • Finished work queues up in the callback (macrotask) queue — timers, clicks — or the microtask queue — promise callbacks.
  • Golden rule: once the stack is empty, the entire microtask queue drains before one macrotask runs.
  • So setTimeout(fn, 0) never means “now” — it means “after current code and every pending microtask.”
event-loop.js
let count = 0;

console.log("sync start", ++count);        // sync start 1

setTimeout(() => {
  console.log("macrotask", ++count);       // runs LAST
}, 0);

Promise.resolve().then(() => {
  console.log("microtask", ++count);       // runs before the timer
});

console.log("sync end", ++count);          // sync end 2

// Output:
// sync start 1
// sync end 2
// microtask 3   ← microtask queue drains first
// macrotask 4   ← even with a 0ms delay
Say it like this

“All the synchronous code runs first. Then, once the stack is empty, the event loop drains the entire microtask queue — promise callbacks — before taking a single item from the callback queue. That's why a promise .then() always beats a setTimeout with zero delay, no matter which was scheduled first.”


Output-Prediction Drill

Predict the output before expanding. Six snippets, one per high-risk area — this is the exact format most JS interviews use.

1. What logs, and why?
drill-1.js
console.log(count);
var count = 5;
console.log(count);

undefined, then 5. In the memory creation phase count was created and set to undefined; the assignment to 5 only happens when the code phase reaches that line.

2. What logs, and why?
drill-2.js
function counter() {
  let count = 0;
  count++;
  return count;
}
console.log(counter(), counter(), counter());

1 1 1. Every call creates a fresh variable environment, so count starts at 0 each time. No closure is involved — nothing is retained between calls.

3. What logs, and why?
drill-3.js
function createCounter() {
  let count = 0;
  return () => ++count;
}
const a = createCounter();
const b = createCounter();
console.log(a(), a(), b());

1 2 1. a holds a closure over its own count, so it increments across calls. b is a separate call to createCounter, so it gets a completely independent count starting at 0.

4. What logs, and why?
drill-4.js
let count = 1;
{
  var count2 = 2;
  let count3 = 3;
}
console.log(count2);
console.log(count3);

2, then ReferenceError: count3 is not defined. var ignores the block and leaks out to the enclosing scope; let is confined to the block and no longer exists once it ends.

5. What logs, and why?
drill-5.js
console.log("A");
setTimeout(() => console.log("B"), 0);
Promise.resolve().then(() => console.log("C"));
console.log("D");

A, D, C, B. Synchronous code first (A, D). Then the stack empties and the microtask queue drains completely (C). Only then does one macrotask run (B) — the 0ms delay doesn't change the priority.

6. What logs, and why?
drill-6.js
for (var i = 1; i <= 3; i++) {
  setTimeout(() => console.log(i), 0);
}

4, 4, 4. One shared i; by the time the callbacks run, the loop has finished and i is 4. The one-word fix: change var to let, which creates a fresh binding per iteration → 1, 2, 3.


Rapid-Fire Q&A

Read the question, answer out loud, then expand to check. If you stumble, that's your re-read list.

What are the two phases of an execution context?

Memory creation phase (vars get undefined, function declarations get their full body) and code execution phase (runs line by line).

Are let and const hoisted?

Yes — but into a separate, uninitialized state called the Temporal Dead Zone, so accessing them early throws "Cannot access before initialization" instead of returning undefined.

Difference between undefined and not defined?

undefined = the variable exists but has no value yet. not defined = it was never declared; accessing it throws a ReferenceError.

Why can I mutate an object declared with const?

const locks the binding, not the contents. The variable can't point somewhere new, but the object it points to is still mutable.

Is the scope chain decided at call time or author time?

Author time — JavaScript is lexically (statically) scoped. Where the function is written determines what it can see, not where it's called from.

What exactly is a closure?

A function together with a live reference to the lexical environment it was created in, which keeps those variables alive even after the outer function has returned and been popped off the stack.

Does a closure capture a value or a reference?

A reference. If the outer variable changes after the closure is created, the closure sees the new value — that's exactly why the var-loop bug prints the final value.

Two calls to the same factory — do they share a closure?

No. Each call creates its own execution context and its own lexical environment, so each returned function gets a completely independent set of variables.

Can closures cause memory leaks?

Yes — anything still referenced by a live closure can't be garbage collected. A common real case is an event listener that's never removed, keeping its entire surrounding scope alive.

Why does removeEventListener sometimes do nothing?

Because it was passed a different function reference. It must receive the exact same function object that was added — an identical-looking new arrow function won't match.

Name V8's interpreter, optimizing compiler, and garbage collector.

Ignition, TurboFan, and Orinoco. Ignition and TurboFan work simultaneously — that's what JIT compilation means.

Is setTimeout part of JavaScript?

No. It's a Web API provided by the browser (or by Node's runtime), exposed through the global object. The engine itself only has the call stack and heap.

Promise .then() vs setTimeout(fn, 0) — which runs first?

The promise callback. It's a microtask, and the entire microtask queue drains before a single macrotask (the timer) is allowed to run.

What is starvation in the event loop?

When microtasks keep scheduling more microtasks, the queue never empties — so the callback queue and even browser rendering never get a turn.

Is JavaScript single-threaded or multi-threaded?

Single-threaded — one call stack, one thing at a time. Concurrency comes from offloading work to Web APIs and scheduling the results back through the queues, not from extra JS threads. (Web Workers do run JavaScript on other threads, but each worker has its own stack and cannot touch your variables directly.)


That's the entire execution model — memory phase to microtask queue. You built it lesson by lesson; this was just the fast lap.

All 14 full lessons