Skip to main content
CodeOath
← All posts

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:

varletconst
ScopeFunctionBlockBlock
HoistingHoisted, initialized to undefinedHoisted, but in the TDZ until declaredHoisted, but in the TDZ until declared
Re-declaration in the same scopeAllowed (silently overwrites)SyntaxErrorSyntaxError
Re-assignmentAllowedAllowedTypeError — the binding can't be reassigned
Attached to window/global object (browser, top-level script)YesNoNo

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.