# Scope

## 1. What Scope Controls

Scope determines **where a variable is visible and accessible** in your code. JavaScript resolves variable lookups using **lexical (static) scoping**: visibility is determined by where code is *written*, not where it's *called from*.

## 2. The Three Kinds of Scope

| Scope type | Created by | Applies to |
|---|---|---|
| **Global scope** | Code outside any function/block | `var`, `let`, `const` |
| **Function scope** | Any `function` | `var`, `let`, `const` |
| **Block scope** | `{ }` — `if`, `for`, `while`, bare blocks | `let`, `const` only (**not** `var`) |

```js
let global = "visible everywhere";

function outer() {
  let functionScoped = "visible anywhere inside outer()";

  if (true) {
    let blockScoped = "visible only inside this if-block";
    var notBlockScoped = "visible anywhere inside outer(), NOT just this block";
  }

  console.log(notBlockScoped); // works — var ignores block boundaries
  // console.log(blockScoped);  // ReferenceError
}
```

## 3. The Scope Chain

When a variable is referenced, the engine searches **outward**: current scope → enclosing scope → ... → global scope. The first match wins. If nothing matches, JavaScript throws a `ReferenceError` (in strict mode) or creates an accidental global (in non-strict mode, `var`/undeclared only).

![Nested global, function, and block scopes with the lookup direction searching outward](diagrams/01-scope-chain.svg)

```js
let a = "outer";

function level1() {
  let b = "level1";

  function level2() {
    let c = "level2";
    console.log(a, b, c); // "outer level1 level2" — all reachable via the chain
  }
  level2();
}
level1();
```

Scope chains are **one-directional**: inner scopes see outer variables, but outer scopes cannot see inner variables.

## 4. Shadowing

An inner scope can declare a variable with the same name as an outer one. The inner variable **shadows** (hides) the outer one for the rest of that inner scope.

```js
let x = "outer";
function shadow() {
  let x = "inner";
  console.log(x); // "inner" — the outer x is inaccessible here
}
shadow();
console.log(x); // "outer" — unaffected
```

## 5. Lexical Scoping vs Dynamic Scoping

JavaScript is **lexically scoped**: a function's access to variables is determined by where the function is **defined**, not where it's **called**.

```js
let value = "global";

function printValue() {
  console.log(value); // always looks up `value` from where printValue was DEFINED
}

function wrapper() {
  let value = "local to wrapper";
  printValue(); // still prints "global" — not "local to wrapper"
}
wrapper();
```

This is the mechanism that makes closures possible (see the Closures topic).

## 6. Module Scope

Each ES module (`.mjs`, or `.js` with `"type": "module"`) has its own top-level scope. Top-level `let`/`const`/`var`/`function` declarations are **not** added to the global object and are invisible to other modules unless explicitly exported.

## 7. Under the Hood: Scope Chains Are Real Objects at Runtime

"Scope" isn't just a conceptual rule the engine follows loosely — at runtime, the scope chain is a real linked structure of objects the engine walks to resolve every variable reference.

### Compile time: building the scope tree

Before running anything, V8's parser builds a static **scope tree** matching the nesting of your functions and blocks. For each scope, the compiler determines exactly which variables it declares and — critically — whether any of them are **captured by an inner function** (referenced from a nested closure).

### Runtime: Context objects vs stack slots

This compile-time analysis feeds a per-variable decision:

| Variable's fate | Where it lives | Why |
|---|---|---|
| Never captured by any inner closure | a fast **stack slot / CPU register** | can be discarded the instant the function returns — no heap needed |
| Captured by at least one inner closure | a heap-allocated **`Context` object** | must outlive the function call, since the closure might run later |

```js
function outer() {
  let uncaptured = "fast";      // stays a stack slot — outer() returning discards it immediately
  let captured = "slow-ish";     // promoted to a Context object — an inner function needs it later

  return function inner() {
    return captured;              // this closure forces `captured` (and ONLY `captured`) onto the heap
  };
}
```

Only `captured` gets heap-allocated — `uncaptured` is never touched by the inner function, so the compiler leaves it as a cheap stack slot. This refines the common "closures keep the whole scope alive" simplification: modern engines are precise about *which specific bindings* actually need to survive, not the entire scope wholesale (full detail in the Closures topic).

### The scope chain as a linked list of Context objects

Each `Context` object holds a fixed-size array of slots for its captured variables, plus a pointer to its **parent Context** — mirroring the lexical nesting exactly. Resolving `a` from three functions deep means: check the current Context's slots, if not found follow the parent pointer, repeat — literally walking a linked list of objects, one hop per enclosing scope, until found or the chain ends at the global object.

```
Context(level2) --parent--> Context(level1) --parent--> Context(global)
   [c]                          [b]                          [a]
```

This is *why* deeply nested scope access has a real (if usually negligible) runtime cost proportional to nesting depth — each unresolved lookup means one more Context hop, unlike a stack slot access, which is a single fixed-offset read.

### Global scope is special: it's backed by a real object

Top-level `var` declarations (in non-module scripts) become actual properties on the global object (`window`/`globalThis`) — which is why they're visible as `window.myVar`. Top-level `let`/`const` are NOT added to the global object (they live in a separate, non-enumerable "script scope" record) — a real, observable difference, not just a stylistic one.

## 8. Common Pitfalls

1. **`var` inside loops** — a single `var` binding is shared across all iterations, which breaks closures capturing the loop variable (see Closures exercises).
2. **Accidental globals** — assigning to an undeclared variable in non-strict mode silently creates a global. Always use `"use strict"` or ES modules (strict by default).
3. **Confusing function scope with block scope** — a `var` declared inside a `for` loop is visible outside the loop entirely.
4. **Shadowing bugs** — redeclaring a parameter name or an outer variable name inside a nested function can silently hide the outer value.

## 9. Quick Reference

```
Global scope
 └─ Function scope (created per function call)
     └─ Block scope (created per { }, let/const only)
```
Lookup direction: **inner → outer**, never outer → inner.
