02 — Variables and Scope — Where a Name Lives
"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:
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 bindingScope: 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 blockThat 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
[2] Mozilla, "var statement," MDN Web Docs, 2024. [Online]. Available: 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
[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/
[5] Mozilla, "Hoisting," MDN Web Docs, 2024. [Online]. Available: 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
Knowledge check · Question 1 of 5
A `const` object's property can be mutated. Why doesn't this throw?
Comments
Leave a Comment
You must be signed in to comment
0 Comments
No comments yet. Be the first to comment!