Frontend Development 8 min read

Why ‘var’ Is Dangerous and How let/const Solve Its Pitfalls

This article examines why the traditional JavaScript var keyword should be avoided, explains the subtle pitfalls of its function scope, hoisting, and global leakage, and highlights the essential, often overlooked features of let and const—including temporal dead zone, true block scope, and immutability nuances.

JavaScript
JavaScript
JavaScript
Why ‘var’ Is Dangerous and How let/const Solve Its Pitfalls

Currently, let and const have replaced var, bringing better scope rules and stricter usage, yet even experienced developers overlook subtle details.

var problems: why not use it

Before diving into let and const, we need to understand why we shouldn't use var:

Function scope instead of block scope

<code>if (true) {
  var x = 10;
}
console.log(x); // 10, variable x leaks to outer scope</code>

Hoisting confusion

<code>console.log(x); // undefined, not error
var x = 5;
</code>

Allow duplicate declarations

<code>var user = "张三";
var user = "李四"; // no error, silent overwrite
</code>

Global declarations become properties of the global object

<code>var global = "我是全局变量";
console.log(window.global); // "我是全局变量" (browser)
</code>

These characteristics cause many hard‑to‑track bugs, especially in large applications.

let core features: overlooked details

1. Temporal Dead Zone (TDZ)

This is probably the most easily missed feature of let:

<code>console.log(x); // ReferenceError: x is not defined
let x = 5;
</code>

Unlike var, a variable declared with let exists in a "temporal dead zone" from the start of the block until its declaration, making it inaccessible.

<code>let x = 10;
function example() {
  // x is in TDZ here
  console.log(x); // ReferenceError
  let x = 20; // x leaves TDZ
}
</code>

Even if an outer scope already has a variable with the same name, the inner TDZ still prevents access.

2. True block scope

let strictly follows block‑scope rules, which is often underestimated:

<code>for (let i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// output: 0, 1, 2

for (var i = 0; i < 3; i++) {
  setTimeout(() => console.log(i), 100);
}
// output: 3, 3, 3
</code>

Each loop iteration creates a new binding for let, which is crucial when dealing with closures.

3. Does not pollute the global object

Although let does not become a property of the global object, a subtle detail is often missed:

Global let variables are stored in a special "script scope" environment rather than on the global object.

const core features: misunderstood immutability

1. Objects and arrays are still mutable

Many developers mistakenly think that const‑declared objects or arrays are completely immutable:

const only guarantees that the binding cannot be reassigned; the contents can still change. To create an immutable object, use Object.freeze():

Note that Object.freeze() performs only a shallow freeze.

Deep freezing requires recursively applying Object.freeze().

2. Must be initialized at declaration

The timing of initialization is often overlooked:

Although a const variable cannot be reassigned, its contents are not immutable.

3. Performance considerations

In some JavaScript engines, const declarations can offer slight performance benefits:

The engine can treat these values as never changing, enabling optimizations such as constant folding.

Practical usage patterns and best practices

1. Default to const, fall back to let when needed

This approach minimizes variable reassignment, improving readability and maintainability.

2. Destructuring with let and const

Function parameter destructuring is essentially a const declaration and cannot be reassigned.

3. Loops: let vs const

Using const in for‑of and for‑in loops is valid because each iteration creates a new binding.

Deep dive: internal mechanics of let and const

Understanding how JavaScript engines handle these declarations helps avoid common traps:

<code>// Simplified internal processing flow
function example() {
  // 1. Create lexical environment
  // 2. Mark let/const declarations as "uninitialized" (TDZ starts)

  // console.log(x); // Uncommenting would throw: x is in TDZ

  let x = 10; // x leaves TDZ and is assigned 10

  if (true) {
    // Create new block lexical environment
    const y = 20;
    x = 30; // Can access outer x
    // y is only available in this block
  }

  // console.log(y); // Uncommenting would throw: y is not in this scope
}
</code>

The distinction between Lexical Environment and Variable Environment determines how the engine treats different declaration types.

JavaScriptbest practicesconstletscopehoistingvar
JavaScript
Written by

JavaScript

Provides JavaScript enthusiasts with tutorials and experience sharing on web front‑end technologies, including JavaScript, Node.js, Deno, Vue.js, React, Angular, HTML5, CSS3, and more.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.