Skip to main content

Menu

System onlineAI Tutorials / 2026

AI Tutorials
Knowledge search

Recent · Popular · Tutorials

  • dart3 tutorials
  • java1 tutorial
  • Javascript3 tutorials
  • Node.js Introduction2 tutorials
  • dartdart · General · In depth
  • JavascriptJavascript · Advanced · In depth
  • JavascriptJavascript · Advanced · Mid
  • JavascriptJavascript · Advanced · Short

Find a tutorial

Tell the assistant what you want to learn. It suggests published chapters.

For example: “I want to learn Rust ownership from the start.” The conversation is kept only in this tab.

0 / 2,000 characters

Enter to send, Shift+Enter for a new line. Do not include personal information.

Chapter 2

Variables, let, const, and Scope

This chapter assumes you already know what a variable is, have read the chapter on values and types, and are comfortable reading short JavaScript snippets. We are working at the depth of the specification: lexical environments, the temporal dead zone, hoisting semantics, and the shadowing and module
Level: AdvancedLength: In depth4 min read

Read at another depth

Variables, let, const, and Scope

Assumed prerequisites

This chapter assumes you already know what a variable is, have read the chapter on values and types, and are comfortable reading short JavaScript snippets. We are working at the depth of the specification: lexical environments, the temporal dead zone, hoisting semantics, and the shadowing and module-scope interactions that cause real bugs in production code.

The execution model: lexical environments

JavaScript resolves every variable reference through a chain of lexical environments. An environment is a record mapping names to bindings, created each time a function runs, a block executes, a module loads, or a script starts. When code reads an identifier, the engine looks it up in the current environment, then the enclosing one, and so on up to the global environment. This is why scope is lexical — determined by where the code is written, not how it is called.

  • let and const create declarative bindings: mutable and immutable respectively, block-scoped, uninitialized until their declaration executes.
  • var creates bindings on the enclosing function (or global) environment, initialized to undefined immediately.
  • At the top level of a module (or a classic script), let/const/class go into the global lexical environment — a separate layer from the global object — while var and function declarations become properties of the global object. This is why let x = 1 at top level does not create window.x.
Code example in javascript
// Module scope vs global object
let lexical = 'in global lexical environment';
var globalProp = 'on globalThis';

console.log(globalThis.globalProp); // 'on globalThis'
console.log(globalThis.lexical);    // undefined

Three declaration forms, three behaviors

This part of the chapter cannot be displayed here.

Two clarifications that are commonly muddled:

  1. const makes the binding immutable, not the value. Objects and arrays declared with const can still be mutated; you cannot reassign the name.
  2. Redeclaration of let in the same lexical scope is a static error, but the same name can legally appear in sibling scopes and in nested scopes (shadowing).
Code example in javascript
const config = { host: 'localhost' };
config.host = 'prod.internal'; // fine — mutating the object
// config = {};                 // TypeError: assignment to constant variable

function shadowing() {
  let x = 'inner';
  {
    let x = 'shadowed';
    console.log(x); // 'shadowed'
  }
  console.log(x);   // 'inner'
}

Hoisting and the temporal dead zone

All declarations are hoisted in the sense that the engine knows about them before executing the containing block. The difference is when the binding is initialized:

  • var bindings are initialized to undefined at function entry. Reading them early yields undefined.
  • let/const bindings are created at block entry but remain uninitialized until the declaration statement runs. Reading them before that is a ReferenceError. This window is the temporal dead zone (TDZ).
  • function declarations are fully hoisted: initialized and usable at the top of the scope.
Code example in javascript
console.log(a); // undefined — var is initialized at entry
var a = 1;

console.log(b); // ReferenceError — TDZ
let b = 2;

{
  console.log(c); // ReferenceError, even though c exists in this block
  let c = 3;
}

The second snippet is worth dwelling on: inside the block, the engine finds the let c binding and its TDZ status before reaching the outer scope. TDZ errors are therefore "binding found but not ready," not "binding not found." This lets tools and lint rules catch shadowing-related footguns.

Getting parameters into the TDZ picture

Function parameters are initialized before the function body runs, but default parameter expressions are evaluated in an inner scope and can themselves trigger TDZ errors:

Code example in javascript
function f(a = b, b = 2) {} // ReferenceError when called with no args

`var` in the modern codebase

var has one legal-but-poorly-understood behavior left that matters in reviews: it ignores block scope, which is exactly what made the classic loop-closure bug possible.

Code example in javascript
// With var: every closure captures the same binding
const fs = [];
for (var i = 0; i < 3; i++) fs.push(() => i);
console.log(fs.map((f) => f())); // [3, 3, 3]

// With let: a fresh binding per iteration
const gs = [];
for (let i = 0; i < 3; i++) gs.push(() => i);
console.log(gs.map((f) => f())); // [0, 1, 2]

Mechanically, for (let ...) creates a new environment per iteration and copies the previous value into it before the update expression runs. That per-iteration copy is specified behavior, not luck.

Decision criteria

  • Default to const for every binding you do not reassign. It makes data flow explicit: the reader knows the name never changes meaning.
  • Use let only for genuine reassignment: loop counters, accumulators, swap patterns.
  • Avoid var except when maintaining legacy code you cannot convert; lint rules (no-var) enforce this.
  • Consider that const + mutation is often a smell; Object.freeze gives shallow immutability when you want it (nested objects still need freezing recursively).

callout note The const-by-default habit is not about preventing runtime errors — reassigning an object's binding is rarely what breaks code. It is about documentation: every const is a promise to the reader that this name is stable, which makes refactorings and dependency analysis cheaper. callout

callout warning TDZ errors throw a ReferenceError at runtime, and only when the read actually executes. A TDZ violation inside a branch that never runs is invisible. Linters catch the static cases, but do not assume a passing test proves TDZ safety. callout

Exercises

Exercise

In a classic script (non-module), let x = 1; and var x = 2; both at top level — what happens, and why?

Show answer

A SyntaxError. var x creates a property on the global object, while let x creates a binding in the global lexical environment, but the spec still treats redeclaring the same name across these layers as a conflict; var x = 2; let x = 1; fails the same way. (The reverse-order example, let x = 1; var x = 2;, throws at the point the var is processed — also a SyntaxError.)

Exercise

Predict the output of the classic loop-closure bug with var, and explain precisely why let fixes it.

Show answer

With var i, all three closures capture the same function-scope binding, which ends at 3, so output is [3, 3, 3]. With let i, the loop spec creates a fresh environment per iteration and copies the prior value into it, so each closure sees its own 0, 1, 2.

Exercise

Why does console.log(c) inside the block shown above throw a ReferenceError even though c is visible later in the same block?

Show answer

The block has its own declarative environment containing the let c binding, but that binding is uninitialized (TDZ) until the declaration statement executes. Lookup finds the binding and throws rather than falling through to any outer c.

Spotted a mistake, or code that does not work?

Report an issue

Chapter: Variables, let, const, and Scope

0 / 2,000 characters

Plain text, for example: the second code sample does not compile. Do not include personal information.

You can't do that