Skip to main content

Command Palette

Search for a command to run...

JS Fundamentals #16.1 var vs let inside loop and closures

Published
2 min read

1. Lexical Environment (LE) Basics

  • A lexical environment is a container where JS stores variable bindings and a reference to its outer environment.

  • New LE creation is triggered by:

    1. Function execution → each function call gets a new LE.

    2. Block scope with let/const → each {} with block-scoped variables creates a new LE.

  • var does NOT trigger a new LE for blocks; it is function-scoped.


2. Loops and Lexical Environments

Using var:

for (var i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}
  • var i is function-scoped.

  • The for loop does NOT create a new LE per iteration.

  • All iterations share the same i binding in the function/global LE.

  • When setTimeout callbacks execute, they all refer to this single i, which has the final loop value.

Result with var: All callbacks print the same value (e.g., 3 if the loop ended at 3).


Using let:

for (let i = 0; i < 3; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}
  • let i is block-scoped.

  • ES6 specification creates a new lexical environment for each iteration of the loop.

    • Each iteration’s i is a new binding.
  • Each setTimeout callback closes over its iteration’s LE, so it remembers the correct value of i.

Result with let: Callbacks print 0, 1, 2 respectively.


3. How setTimeout interacts with LEs

  1. When the callback function is created, the JS engine attaches a hidden [[Environment]] pointing to the LE where it was created.

  2. When the callback executes later (asynchronously), JS looks up variables via the scope chain starting from that [[Environment]].

  3. Key difference:

    • With var: single shared LE → all callbacks see the same i.

    • With let: separate LE per iteration → callbacks see the correct i.


✅ Summary Table

Featurevarlet
ScopeFunction/globalBlock
LE creation per iteration❌ no✅ yes
Loop iteration variableShared bindingNew binding per iteration
Closure behavior in async callbacksAll callbacks share same valueEach callback remembers its own value

Key Takeaway:

The creation of a new lexical environment is triggered by block-scoped variables (let/const). In loops, this ensures each iteration has a fresh binding, allowing closures like setTimeout callbacks to “remember” the correct variable value.
var doesn’t do this because it’s function-scoped, so all iterations share the same binding.