Legacy Features to Avoid

This section documents modern ECMAScript and core browser JavaScript APIs — it is not tied to any specific framework or library (React, Vue, Angular, etc.). This content was generated with the assistance of AI and should be verified against the current ECMAScript specification and MDN documentation before relying on it in production, since JavaScript language features and browser API support continue to evolve.

This section’s bibliography lists the reference material consulted while preparing these pages.

JavaScript’s standard library and syntax have grown incrementally since the language’s earliest days, and ES6 (2015) in particular replaced several long-standing patterns with cleaner alternatives. Because old code (and old tutorials) still use the legacy forms, it helps to recognize them on sight — not to write new code with them. The rest of this reference assumes the modern syntax throughout; this page exists so later pages do not need to re-justify that choice each time.

var vs. let/const

Before ES6, var was the only way to declare a variable, and it behaves quite differently from let and const:

function example() {
  if (true) {
    var x = 1;   // function-scoped: visible for the whole function body
    let y = 2;   // block-scoped: only visible inside this if-block
  }
  console.log(x); // 1 -- still accessible
  console.log(y); // ReferenceError: y is not defined
}

Function scoping instead of block scoping

A var declaration ignores the block ({ }) it is written in and attaches itself to the nearest enclosing function — or, at the top level, to the global object. This means a var declared inside an if block, a for loop, or any other block is just as visible outside that block as inside it, which makes it easy to lose track of where a variable actually "belongs" as a function grows. let and const fix this by scoping the variable strictly to the block it is declared in, matching the block-scoping behavior most other C-family languages already have.

Hoisting pitfalls

A var declaration is hoisted: the declaration itself is moved to the top of the enclosing function at parse time, while the initialization stays where it was written. The practical effect is that the variable name is usable (as undefined) anywhere in the function, even in code that runs before the var statement itself:

console.log(a); // undefined -- no error, but almost certainly a bug
var a = 5;

console.log(b); // ReferenceError: Cannot access 'b' before initialization
let b = 5;

let and const are also hoisted to the top of their block, but the region between the top of the block and the actual declaration — the "temporal dead zone" — throws a ReferenceError on any access, surfacing the bug immediately instead of silently producing undefined.

var also tolerates redeclaring the same name multiple times in the same scope without error, which let/const correctly reject as a syntax error — another case where `var’s permissiveness hides mistakes that the modern keywords catch at parse time.

There is no remaining reason to reach for var in new code. Use const by default, and let only for variables that are genuinely reassigned.

Prototype-Based "Classes" Superseded by class

Before ES6 introduced the class keyword, JavaScript’s only inheritance mechanism was its prototype chain, and object-oriented code had to build "classes" on top of it manually — either by writing a constructor function and attaching methods to its .prototype object, or by using Object.create() to link one plain object to another as its prototype:

// Legacy: constructor function + manual .prototype assignment
function Vehicle(wheels) {
  this.wheels = wheels;
}
Vehicle.prototype.describe = function () {
  return `A vehicle with ${this.wheels} wheels`;
};

function Car() {
  Vehicle.call(this, 4);       // manual "super" call
}
Car.prototype = Object.create(Vehicle.prototype); // manual inheritance link
Car.prototype.constructor = Car;                  // manual bookkeeping

The equivalent written with class (covered in full in Classes) makes the same prototype-chain mechanism explicit and far less error-prone: no manual .prototype wiring, no easy-to-forget constructor bookkeeping, and a real super() call instead of Vehicle.call(this, …​):

class Vehicle {
  constructor(wheels) {
    this.wheels = wheels;
  }
  describe() {
    return `A vehicle with ${this.wheels} wheels`;
  }
}

class Car extends Vehicle {
  constructor() {
    super(4);
  }
}

class syntax does not add a new inheritance model — under the hood it is still the same prototype chain — but it removes the boilerplate and footguns of wiring that chain up by hand, so new code should always prefer it over constructor-function patterns or bare Object.create() chains.

Other Legacy Patterns

A few smaller legacy patterns are worth being able to recognize, even though they are covered in more depth elsewhere in this reference:

  • The arguments object. Every non-arrow function used to expose its call-time arguments through an array-like (but not actually an array) arguments object. Rest parameters (function f(…​args) { }) give the same information as a real array, with clearer syntax and no confusion in nested/arrow functions where arguments refers to the enclosing function’s arguments rather than the current one.

  • var-based IIFE namespacing. Before ES modules existed, code avoided polluting the global scope by wrapping entire files in an "immediately invoked function expression" ((function () { …​ })();) and manually attaching whatever needed to be shared onto a single global namespace object. ES modules (see Modules) give every file its own scope natively, via import/export, making this pattern unnecessary.

None of these legacy forms are technically deprecated — var, arguments, and manual prototype wiring all still work in current JavaScript engines, and you will still encounter them in older codebases. They are simply superseded: the modern alternatives are safer by default and are what the rest of this reference assumes.