# Hoisting

## 1. What Hoisting Actually Is

Before executing any code, the JavaScript engine scans the current scope and registers all variable and function declarations in memory. This is **hoisting** — declarations behave as if they were moved to the top of their scope. What differs between `var`, `let`, `const`, and `function` is **what gets registered** and **what value it starts with**.

![Phase 1 creation scans and registers every declaration; phase 2 execution runs statements and assigns real values](diagrams/01-two-phase-compilation.svg)

## 2. Hoisting by Declaration Type

| Declaration | Hoisted? | Initial value | Usable before the line? |
|---|---|---|---|
| `var` | ✅ | `undefined` | ✅ yes (reads as `undefined`) |
| `let` | ✅ | none (TDZ) | ❌ ReferenceError |
| `const` | ✅ | none (TDZ) | ❌ ReferenceError |
| `function` declaration | ✅ | the full function | ✅ yes (fully callable) |
| `function` expression / arrow assigned to `let`/`const` | only the variable | none (TDZ) | ❌ ReferenceError |
| `class` declaration | ✅ | none (TDZ) | ❌ ReferenceError |

```js
console.log(a);        // undefined — var is hoisted and initialized
var a = 1;

console.log(b);        // ReferenceError — TDZ
let b = 2;

greet();                // works! function declarations are fully hoisted
function greet() { console.log("hi"); }

sayBye();                // TypeError: sayBye is not a function
var sayBye = function () { console.log("bye"); };
```

## 3. Why `function sayBye` Above Throws a Different Error

`var sayBye` is hoisted and initialized to `undefined`. The **assignment** of the function expression happens later, at the original line. So calling `sayBye()` too early calls `undefined()`, which is a `TypeError`, not a `ReferenceError`.

## 4. Function Declarations vs Function Expressions

```js
// Declaration — hoisted whole, callable anywhere in scope
function declared() { return "I work everywhere in this scope"; }

// Expression — only the variable is hoisted, not the function body
const expressed = function () { return "I only work after this line"; };

// Arrow function — same rule as expressions
const arrow = () => "same as above";
```

## 5. `let`/`const`/`class` and the TDZ

These are hoisted (the engine knows they exist) but stay **uninitialized** until their declaration line executes. Referencing them earlier throws — this protects against relying on a variable before it has a real value, unlike `var`'s silent `undefined`.

```js
{
  // TDZ for `x` starts here
  console.log(typeof x); // ReferenceError — NOT "undefined"
  let x = 5;              // TDZ ends here
}
```

Note: `typeof` on a **truly undeclared** variable returns `"undefined"` safely. `typeof` on a variable **in its TDZ** throws. This is a subtle but important distinction.

## 6. Hoisting Inside Blocks

`function` declarations inside blocks (`if`, `for`, etc.) have inconsistent, engine-dependent hoisting behavior in non-strict mode. **Always use function expressions or arrow functions inside blocks** to avoid relying on this.

## 7. Under the Hood: The Two-Pass Compilation That Makes Hoisting Work

Hoisting isn't the engine "moving code around" — it's a direct consequence of how JavaScript is compiled in **two distinct passes** before your code actually runs.

### Pass 1 — Creation Phase (binding instantiation)

Before executing a single statement, the engine scans the entire scope (function or block) and registers every declaration it finds — `var`, `let`, `const`, `function`, `class` — into that scope's **Environment Record** (V8's Context or stack-slot equivalent, see the Scope topic). This is a purely structural scan; no right-hand-side expressions are evaluated yet.

During this pass, each binding gets a slot, initialized differently depending on its kind:

```
var x;              -> slot "x" created, initialized to undefined immediately
let y;               -> slot "y" created, marked with the TDZ sentinel ("the hole")
function f(){...}     -> slot "f" created AND fully initialized with the function object, right away
class C {}             -> slot "C" created, marked with the TDZ sentinel, same as let/const
```

This single pass explains every hoisting rule from section 2 at once: `var` gets a real (if useless) default value immediately; `function` declarations get their *entire* value immediately (because the whole function body is already known at parse time — nothing needs to be "evaluated" to build a function object from a declaration); `let`/`const`/`class` get a slot but no usable value yet.

### Pass 2 — Execution Phase

Only now does the engine run your code top to bottom, statement by statement. A `var x = 5;` line, at this point, is really just an **assignment** to the slot that Pass 1 already created — the "declaration" part already happened. A `let y = 5;` line does two things at once: it removes the TDZ sentinel from `y`'s slot AND assigns the value.

### Why this two-pass model produces the exact errors it does

```js
console.log(typeof x); // "undefined" — slot exists (Pass 1), holds undefined, no error
var x = 5;

console.log(typeof y); // ReferenceError — slot exists (Pass 1), but holds the TDZ sentinel
let y = 5;
```

`typeof` on a truly nonexistent binding returns `"undefined"` safely — but `typeof` on a binding that exists-but-is-in-its-TDZ hits the sentinel check and throws. The difference between "doesn't exist" and "exists but not yet initialized" is the entire hoisting mechanism in one sentence, and it's enforced by a real runtime check against that sentinel value, not a special-cased rule in the parser.

### Function declarations inside blocks: why behavior varies

Historically (pre-ES6), engines didn't agree on how to hoist a `function` declared inside an `if` block — some hoisted it to the enclosing function scope (like `var`), others scoped it to the block. ES2015+ standardized "Annex B" semantics for non-strict-mode compatibility: the function is block-scoped for `let`-style lookups AND a `var`-style binding pointing at it is *also* created in the enclosing function scope, updated whenever the block executes. This dual-binding behavior is exactly why relying on it across engines/strict-mode settings is fragile — strict mode drops the Annex B `var`-style binding entirely.

## 8. Common Pitfalls

1. Assuming `let`/`const` are "not hoisted" — they are hoisted, they're just uninitialized (TDZ). This distinction matters for interviews and for reasoning about `typeof`.
2. Calling a function-expression-based function before its assignment line, expecting declaration-style hoisting.
3. Declaring `var` inside an `if` block and expecting it to disappear after the block — it doesn't; it's hoisted to the function top.
4. Relying on hoisted `function` declarations inside blocks across different engines/strict-mode settings.

## 9. Quick Reference

```
var        → hoisted, initialized to undefined
let/const  → hoisted, uninitialized (TDZ) until declaration line
function   → hoisted with full body, usable immediately
class      → hoisted, uninitialized (TDZ), like let/const
```
