These three keywords are often taught together but differ on more than just scope — the full picture is worth having as a single reference:
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 |
const prevents reassigning the variable itself, but it does not make the value immutable — an object or array held in a const binding can still have its contents changed freely:
const arr = [1, 2, 3];
arr.push(4); // fine — mutating the array's contents, not reassigning `arr`
arr = [5, 6]; // TypeError — this reassigns the binding itself
Object.freeze() (covered in the array/object methods cheat sheet) is the actual tool for preventing mutation of an object's contents — const only locks the variable binding, not the value it points to.
The practical convention in modern JavaScript: default to const everywhere, switch to let only for variables you know will be reassigned (loop counters, accumulators), and avoid var entirely in new code — its function-scoping and silent re-declaration are almost never what you actually want, and let/const catch real bugs at parse time that var would let through silently.