JavaScript140 min total · 15 parts
JavaScript Core Concepts: Scope, Closures, `this`, and the Event Loop
Part 3 of 15 · ~3 min
var vs. let vs. const: A Direct Comparison
Every variable in our search box has to be declared with something, and the choice is not cosmetic. Let us make it concretely, name by name, on the version we have so far.
function setupSearch() {
const input = document.querySelector("#search-input"); // never reassigned → const
const resultsList = document.querySelector("#search-results"); // never reassigned → const
const recentQueries = []; // the array is mutated, never reassigned → const
let latestQuery = ""; // genuinely changes on every keystroke → let
// var — nothing here. Not one.
}
That is the rule in practice: default to const, downgrade to let the moment you know a name will be reassigned, and skip var entirely in new code. But scope is only the first of five differences between them, and the others bite too. Here is the whole picture in one place:
var | let | const | |
|---|---|---|---|
| Scope | Function | Block | Block |
| Hoisting | Hoisted, initialized to undefined | Hoisted, but in the TDZ until declared | Hoisted, but in the TDZ until declared |
| Re-declaration in the same scope | Allowed (silently overwrites) | SyntaxError | SyntaxError |
| Re-assignment | Allowed | Allowed | TypeError — the binding can't be reassigned |
Attached to window/global object (browser, top-level script) | Yes | No | No |
The row people misread is the last meaningful one, so look closely at recentQueries above. It is a const, and we are absolutely going to push things into it:
const recentQueries = [];
recentQueries.push("closures"); // completely fine — we're changing the array's contents
recentQueries.length = 0; // also fine — still the same array object
recentQueries = []; // TypeError — this tries to reassign the binding itself
const locks the binding, not the value. It is a promise that this name will keep pointing at this same object forever — not a promise that the object will hold still. Freezing the contents themselves needs Object.freeze() instead (covered in the array/object methods cheat sheet), and even that only freezes one layer deep. const simply doesn't make that promise.
Which leaves var, and the honest answer is that you should not use it. Not as style advice — because of a specific bug it will write for you. Here is the search box growing its first real feature: click a result row, open that article.
function renderResults(matches) {
resultsList.innerHTML = matches.map((a) => `<li>${a.title}</li>`).join("");
const rows = resultsList.querySelectorAll("li");
for (var i = 0; i < rows.length; i++) {
rows[i].addEventListener("click", function () {
openArticle(matches[i]); // every row opens the SAME article — the last one
});
}
}
Every row in that dropdown opens the last article in the list. Swap var i for let i and every row opens the right one. Nothing else changes — same loop, same handler, same everything.
That is not a quirk you memorise. It is closures and scope interacting, it is the single most instructive bug in this entire reference, and we are going to take it apart properly in two chapters' time, once we have the machinery to explain it rather than just patch it. For now, note it, leave let in, and keep going — the search box has an ordering problem to deal with first.