---
title: "02 — Variables and Scope — Where a Name Lives"
uid: variables-and-scope
tags: ["scope", "roadmap:javascript", "variables", "const", "hoisting", "let", "javascript", "var"]
excerpt: "Scope decides where a name is visible, and var, let, const are three different visibility contracts — with var carrying one hoisting quirk the other two fixed."
date: 2026-08-13T03:28:07+0000
source: https://www.aveshina.my.id/en/blog/variables-and-scope
---

"Three keywords for the same thing" was how I filed var, let, and const, which hid the actual differences. The idea that everything else hangs off: **scope decides where a name is visible, and var, let, and const are three different visibility contracts, with var carrying one hoisting quirk that the other two fixed.** [1]

The framing that finally landed is that "declaration keyword" is really answering two separate questions at once — *where does this name live?* (its scope) and *can it be reassigned?* The answers are independent, and JavaScript's three keywords bundle them in different ways:

```figure
<svg viewBox="0 0 740 260" xmlns="http://www.w3.org/2000/svg" class="my-6 w-full max-w-2xl" role="img" aria-label="Three declaration keywords mapped on two axes: scope (function vs block) and reassignment (reassignable vs constant). var sits in function-scoped plus reassignable. let sits in block-scoped plus reassignable. const sits in block-scoped plus constant.">
  <g font-family="ui-sans-serif, system-ui, sans-serif" text-rendering="geometricPrecision">
    <!-- axes labels -->
    <text x="370" y="22" font-size="11" font-weight="700" fill="#475569" text-anchor="middle">scope →</text>
    <text x="200" y="22" font-size="10" fill="#64748b" text-anchor="middle">function</text>
    <text x="540" y="22" font-size="10" fill="#64748b" text-anchor="middle">block</text>
    <text x="20" y="130" font-size="11" font-weight="700" fill="#475569">reassign ↓</text>

    <!-- grid lines -->
    <line x1="120" y1="40" x2="120" y2="240" stroke="#cbd5e1" stroke-width="1" stroke-dasharray="3,3"/>
    <line x1="120" y1="130" x2="700" y2="130" stroke="#cbd5e1" stroke-width="1" stroke-dasharray="3,3"/>

    <!-- var: function + reassignable -->
    <rect x="150" y="60" width="180" height="60" rx="10" fill="#fee2e2" stroke="#dc2626" stroke-width="1.5"/>
    <text x="240" y="88" font-size="14" font-weight="700" font-family="ui-monospace, monospace" fill="#7f1d1d" text-anchor="middle">var</text>
    <text x="240" y="106" font-size="10" fill="#7f1d1d" text-anchor="middle">function-scoped · reassignable</text>

    <!-- let: block + reassignable -->
    <rect x="390" y="60" width="180" height="60" rx="10" fill="#e0e7ff" stroke="#6366f1" stroke-width="1.5"/>
    <text x="480" y="88" font-size="14" font-weight="700" font-family="ui-monospace, monospace" fill="#1e1b4b" text-anchor="middle">let</text>
    <text x="480" y="106" font-size="10" fill="#1e1b4b" text-anchor="middle">block-scoped · reassignable</text>

    <!-- const: block + constant -->
    <rect x="390" y="160" width="180" height="60" rx="10" fill="#dcfce7" stroke="#16a34a" stroke-width="1.5"/>
    <text x="480" y="188" font-size="14" font-weight="700" font-family="ui-monospace, monospace" fill="#052e16" text-anchor="middle">const</text>
    <text x="480" y="206" font-size="10" fill="#052e16" text-anchor="middle">block-scoped · constant binding</text>
  </g>
</svg>
```

## The three keywords

- **var** — the old way, pre-ES6. Function-scoped (or global if declared outside a function), reassignable, re-declarable, and hoisted with an initial value of undefined [2]. This is the one with the quirks.
- **let** — block-scoped, reassignable, but **not** re-declarable in the same scope. The default when a value needs to change over time.
- **const** — block-scoped, **not** reassignable, not re-declarable. The default when a value shouldn't change [3].

The mental shortcut I use: const by default, let only when reassignment is genuinely needed, var never in new code. That ordering — const first —catches accidental reassignment bugs at write time.

One subtlety on const that used to trip me up: const freezes the **binding**, not the value. A const object can't be reassigned to a different object, but its properties can still be mutated [3].

```
const user = { name: "Ave" };
user.name = "Shina";   // works — mutating the object
user = { name: "X" };  // TypeError — reassigning the binding
```

## Scope: where the name is visible

Scope is the rule for where a name can be reached. JavaScript has three levels that nest [4]:

- **Global** — declared outside any function or block. Reachable everywhere. In browsers these become properties of window. Overuse is a code smell.
- **Function** — declared inside a function. Reachable only inside it. Each call gets a fresh scope.
- **Block** — declared inside { … } (an if, a for, a bare block). Reachable only inside those braces. This is what let and const give you that var doesn't.

The var problem is that it ignores blocks:

```
for (var i = 0; i < 3; i++) { /* … */ }
console.log(i); // 3 — i leaked out of the block to function/global scope

for (let j = 0; j < 3; j++) { /* … */ }
console.log(j); // ReferenceError — j stayed in the block
```

That leak is the historical reason var-in-loops caused bugs that let simply makes impossible.

## Hoisting and the temporal dead zone

Hoisting is the part I had to slow down on, because "hoisted" means two different things for var versus let/const. The engine collects all declarations in a scope *before* executing the code — that collection step is hoisting [5]. The difference is what value the name has *before* its declaration line runs:

- **var** is hoisted and initialized to undefined. So referencing it early returns undefined — silently, no error.
- **let / const** are hoisted but **left uninitialized**. The window between the start of the scope and the declaration line is called the **temporal dead zone** (TDZ), and accessing the name there throws a ReferenceError [5].

```
console.log(a); // undefined  (var, hoisted + initialized)
var a = 5;

console.log(b); // ReferenceError: Cannot access 'b' before initialization (TDZ)
let b = 5;
```

Function declarations get hoisted *with their body*, which is why you can call a function before its definition appears in the source.

## Naming rules

Once scope is sorted, the naming rules are the boring, necessary kind [6]:

- Names can contain letters, digits, $, and _, but can't start with a digit.
- Case-sensitive — user and User are different names.
- Reserved words (class, return, function) can't be used as names.
- Conventions: camelCase for variables and functions, UPPER_SNAKE for constants, descriptive over short.

None of that is clever. It's the discipline of picking a name that says what the thing *is*, so that six months later the code still reads.

## How I use this

The default-to-const rule is the one that actually changed my code. Every name starts as const; the moment I genuinely need to reassign it, it becomes let. var only appears when I'm reading old code. And I've stopped being surprised by "undefined vs ReferenceError" bugs — that difference is exactly the TDZ doing its job, telling me I read a name before I should have.

## References

[1] I. Kantor, "Variables," The Modern JavaScript Tutorial, 2024. [Online]. Available: [https://javascript.info/variables](https://javascript.info/variables)

[2] Mozilla, "var statement," MDN Web Docs, 2024. [Online]. Available: [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/var](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/var)

[3] Mozilla, "const statement," MDN Web Docs, 2024. [Online]. Available: [https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/const)

[4] freeCodeCamp, "Scope in JavaScript – Global vs Local vs Block Scope Explained," 2022. [Online]. Available: [https://www.freecodecamp.org/news/scope-in-javascript-global-vs-local-vs-block-scope/](https://www.freecodecamp.org/news/scope-in-javascript-global-vs-local-vs-block-scope/)

[5] Mozilla, "Hoisting," MDN Web Docs, 2024. [Online]. Available: [https://developer.mozilla.org/en-US/docs/Glossary/Hoisting](https://developer.mozilla.org/en-US/docs/Glossary/Hoisting)

[6] CodeGuage, "JavaScript Variables — Naming tips," 2023. [Online]. Available: [https://www.codeguage.com/courses/js/variables#Tips_for_naming_variables](https://www.codeguage.com/courses/js/variables#Tips_for_naming_variables)

```quiz
Q: A `const` object's property can be mutated. Why doesn't this throw?
- const freezes the binding, not the value — reassigning the name throws, mutating the object does not
- const is only enforced at declaration time
correct: 0
explain: const prevents reassignment of the variable name to a new value. The object the name points to is still mutable. To freeze the object itself, use Object.freeze().

Q: What prints here?
- undefined (var is hoisted and initialized to undefined)
- ReferenceError (temporal dead zone)
correct: 0
explain: var declarations are hoisted AND initialized to undefined. Referencing the name before its line returns undefined silently. let/const would throw a ReferenceError in the same spot.

Q: Why does `for (var i …)` leak `i` outside the loop but `for (let i …)` does not?
- var is function-scoped; let is block-scoped to the loop body
- var is hoisted; let is not hoisted at all
correct: 0
explain: var ignores block boundaries and resolves to the enclosing function (or global). let is scoped to the block, including each iteration of the loop, so it can't escape.

Q: Default declaration order is…
- let by default, const if the value won't change, var when needed
- const by default, let only when reassignment is needed, var never in new code
correct: 1
explain: Start with const. Promote to let only when you genuinely need to reassign. var is a legacy keyword, avoided in new code.

Q: The temporal dead zone (TDZ) is…
- the window between the start of a scope and the let/const declaration line, where accessing the name throws
- the time it takes the engine to hoist a var
correct: 0
explain: let and const are hoisted but not initialized. The stretch of code from the scope's start to the declaration is the TDZ; reading the name there throws ReferenceError.
```
