ES5 to ES6+ Refresher
Huge amounts of existing code, tutorials and Stack Overflow answers are written in pre-2015 JavaScript. This appendix refreshes the ES5 idioms every intermediate developer must be able to read, and shows the modern equivalent of each.
You will refresh
var, function scope, hoisting, and the IIFE/module pattern.- The four
thisrules, andvar self = this. - Constructor functions,
prototype, and ES5 inheritance. - Callbacks,
arguments,==coercion, and ES5 array methods. - A complete ES5 → ES6+ mapping table.
1. var and function scope
var is scoped to the whole function, not the block, and can be re-declared. Loops therefore share one variable.
In modern JS: let/const are block-scoped (Chapter 2).
Quick check: after if (true) { var x = 1; }, is x defined outside the block?
Yes, x is 1. var ignores block boundaries.
2. Hoisting
Declarations move to the top of their scope: var as undefined, and function declarations completely.
In modern JS: let/const have a TDZ instead (Chapter 2).
Quick check: what does console.log(a); var a = 2; print?
undefined. The declaration is hoisted, but the assignment isn't.
3. IIFEs and the module pattern
An IIFE (immediately invoked function expression) created a private scope; returning an object exposed a public API. This was the "revealing module pattern".
In modern JS: ES modules give each file a private scope (Chapter 14), and blocks or closures cover the rest.
Quick check: why the parentheses around function () {…}?
They make it an expression. A statement that starts with function is a declaration, which can't be called immediately.
4. The four this rules, and self = this
this depends on the call: new, then call/apply/bind, then a method call, then a plain call (global or undefined). Inner callbacks lost the outer this, so ES5 code saved it in a variable.
In modern JS: arrow callbacks inherit this (Chapter 4). .bind(this) was the other ES5 fix.
Quick check: what is this inside setTimeout(function () { … }) in sloppy browser code?
The global object (window). It's a plain call made by the timer.
5. Constructor functions and prototype
Before class, a "class" was a function called with new, with shared methods on its prototype.
In modern JS: class Book { constructor() {} label() {} static fromObject() {} } (Chapter 11).
Quick check: what happens if you call Book("Dune", 1) without new in sloppy mode?
this is the global object, so title and price become globals and the call returns undefined. Classes throw instead.
6. ES5 inheritance
In modern JS: class Ebook extends Product, with super(...) and super.describe().
Quick check: why Object.create(Product.prototype) rather than new Product()?
new Product() would run the constructor (with no title) and put its properties on the shared prototype. Object.create just links the chain.
7. Callbacks and "callback hell"
Asynchronous ES5 code passed callbacks, often using Node's error-first convention (err, result). Each step nested one level deeper.
In modern JS: promises plus async/await (Chapter 12), and util.promisify for old APIs.
Quick check: what does the "error-first" convention mean?
The callback's first argument is an error (or null), and the result comes second.
8. The arguments object
In modern JS: rest parameters, (...parts) => parts.join(" + ") (Chapter 4).
Quick check: do arrow functions have their own arguments?
No. They see the enclosing function's arguments, or get a ReferenceError at module top level.
9. == and type coercion
ES5 code often used ==, which converts types before comparing.
In modern JS: === everywhere, except for the idiom x == null (Chapter 5).
Quick check: is "" == 0 true?
Yes. "" converts to the number 0.
10. Defaults with ||
In modern JS: default parameters (which apply only for undefined) and ?? (Chapters 4 and 5).
Quick check: what does greet("Ana", "") return?
"Hi Ana!". The empty string was replaced, which is the classic || bug.
11. ES5 array methods
ES5 already had forEach, map, filter, reduce, some, every and indexOf; they were just written with function expressions.
In modern JS: arrows, includes, find, at, toSorted, flatMap, … (Chapter 8).
Quick check: what's the modern replacement for arr.indexOf(x) !== -1?
arr.includes(x), which also finds NaN.
12. Objects in ES5
In modern JS: { ...base, [`page${"Size"}`]: 20 }, Object.hasOwn, and Object.entries/fromEntries (Chapter 10).
Quick check: why the hasOwnProperty check in the for…in loop?
for…in also visits inherited enumerable keys, and the check keeps only the object's own keys.
13. Strings in ES5
In modern JS: template literals, endsWith, padStart, replaceAll, at (Chapter 6).
Quick check: what's the modern version of the indexOf check above?
"invoice.pdf".endsWith(".pdf").
Recap: ES5 idiom → modern replacement
| ES5 idiom | Modern replacement | Chapter |
|---|---|---|
var | const / let | 2 |
| IIFE / revealing module | ES modules, blocks | 14 |
var self = this, .bind(this) | arrow callbacks, arrow class fields | 4, 11 |
constructor function + prototype | class, with #private and static | 11 |
Child.prototype = Object.create(Parent.prototype) | class Child extends Parent + super | 11 |
callbacks (err, result) | promises, async/await | 12 |
arguments | rest parameters ...args | 4 |
x = x || default | default parameters, ??, ??= | 4, 5 |
== | === (and == null) | 5 |
"a" + b + "c" | `a${b}c` | 6 |
for (var i…) over arrays | for…of, entries() | 3 |
indexOf(x) !== -1 | includes(x) | 8 |
| objects as dictionaries | Map / Set | 9 |
hasOwnProperty + manual copies | Object.hasOwn, spread, structuredClone | 10 |
| hand-written iterators / index loops | generators, iterator helpers | 13 |
Exercises
Exercise 1 🟢 easy refactor it
Modernise: var names = []; for (var i = 0; i < books.length; i++) { names.push(books[i].title); }
Show solution
map plus an arrow replaces the loop, the counter and the push.
Exercise 2 🟢 easy predict the output
What does this print?
Show solution
undefined. The inner var x is hoisted to the top of f, shadowing the global, and it hasn't been assigned yet.
Exercise 3 🟢 easy refactor it
Convert the Book constructor function from section 5 into a class.
Show solution
The prototype method becomes a class method and the function property becomes static; destructuring tidies the parameters.
Exercise 4 🟢 easy explain why
Why did ES5 code write var self = this; before a callback?
Show solution
The callback is a normal function called by someone else, so its this is different (global or undefined). Saving the outer this in a variable lets the closure reach it. Arrow functions make that unnecessary.
Exercise 5 🟡 medium fix the bug
All buttons report 3. Fix it the ES5 way (an IIFE) and the modern way (let).
Show solution
The IIFE captures each value in a new scope; let creates a new binding per iteration automatically.
Exercise 6 🟡 medium refactor it
Rewrite the nested callbacks in section 7 with promises and async/await, using util.promisify.
Show solution
Error-first callbacks map naturally to promises: the error becomes a rejection, which try/catch handles once for all the steps.
Exercise 7 🟡 medium predict the output
What does greet("", 0) return here, and what would it return with default parameters and ???
Show solution
It returns "guest ": both the empty name and 0 were treated as missing. With (name = "guest", times = 1), the result is "", because an empty string repeated 0 times is empty. Whether that's wanted is a design decision; the point is that || decided it for you silently.
Exercise 8 🟡 medium refactor it
Turn the revealing-module Counter (section 3) into an ES module with a named export, and import it.
Show solution
Module scope replaces the IIFE: count is invisible to importers, and only inc is exported.
Exercise 9 🔴 hard explain why
Why does ES5 inheritance need Ebook.prototype.constructor = Ebook? What breaks without it?
Show solution
Object.create(Product.prototype) makes a new object whose inherited constructor property is Product. Code that relies on obj.constructor (for example, to create "another one of these", or to log the class name) would wrongly see Product. class … extends sets this up correctly for you.
Exercise 10 🔴 hard mini-project
Modernise this whole ES5 snippet: var, constructor function, self = this, || defaults, string concatenation and indexOf. Keep the behaviour (except fixing the || bug for a 0% discount).
Show solution
The ES5 version applied a 10% discount even when 0% was requested. The modern version, with a default parameter, respects the 0. A class field replaces the constructor's array set-up, an arrow replaces self, and some replaces the map plus indexOf combination.