Appendix A · Refresher

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 this rules, and var 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 idiomModern replacementChapter
varconst / let2
IIFE / revealing moduleES modules, blocks14
var self = this, .bind(this)arrow callbacks, arrow class fields4, 11
constructor function + prototypeclass, with #private and static11
Child.prototype = Object.create(Parent.prototype)class Child extends Parent + super11
callbacks (err, result)promises, async/await12
argumentsrest parameters ...args4
x = x || defaultdefault parameters, ??, ??=4, 5
===== (and == null)5
"a" + b + "c"`a${b}c`6
for (var i…) over arraysfor…of, entries()3
indexOf(x) !== -1includes(x)8
objects as dictionariesMap / Set9
hasOwnProperty + manual copiesObject.hasOwn, spread, structuredClone10
hand-written iterators / index loopsgenerators, iterator helpers13

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.

Official docs