JavaScript History, Versions & The Foundation: ES3 · ES5 · ES6 · Browser Compatibility
Table of Contents
- The Complete JavaScript Version History
- The Story of JavaScript — From Birth to Dominance
- ES3 (1999) — The Foundation That Ran for a Decade
- ES5 (2009) — The Reliability Revolution
- ES6 / ES2015 — The Modern Language Transformation
- IE and Edge — The Browser Compatibility Story
- Phase 2 — Applied Exercises
- Phase 3 — Project Simulation
- Completion Checklist
PHASE 1 — CONCEPTUAL UNDERSTANDING
1. The Complete JavaScript Version History
1.1 Why Version History Matters
When you write JavaScript today, you are writing inside a language that is 30 years old. Every line you type uses concepts introduced at specific points in time by specific people solving specific problems. Understanding the history:
- Explains why JavaScript has odd behaviours (like
typeof null === "object") - Helps you read older codebases that use
var,arguments, andprototypepatterns - Makes you a better debugger — you know what was possible when each piece of code was written
- Prepares you for technical interviews where history and compatibility questions are common
Real-world analogy: Understanding the history of roads helps a traffic engineer understand why some city layouts are efficient and others are not. The constraints and decisions of the past shaped what exists today.
1.2 The Complete Version Timeline
| Year | Version | Official Name | Significance |
|---|---|---|---|
| 1995 | ES1 pre-release | “Mocha” / “LiveScript” / “JavaScript” | Created in 10 days by Brendan Eich at Netscape |
| 1997 | ES1 | ECMAScript 1 | First official standard published by ECMA |
| 1998 | ES2 | ECMAScript 2 | Minor editorial update to align with ISO standard |
| 1999 | ES3 | ECMAScript 3 | First major feature release — still the baseline for 10 years |
| 2009 | ES5 | ECMAScript 5 | Reliability overhaul — strict mode, JSON, property descriptors |
| 2011 | ES5.1 | ECMAScript 5.1 | Minor editorial revision |
| 2015 | ES6 | ECMAScript 2015 | The biggest leap — classes, arrow functions, Promises, modules |
| 2016 | ES7 | ECMAScript 2016 | Start of annual releases — includes(), ** |
| 2017 | ES8 | ECMAScript 2017 | async/await, Object.entries() |
| 2018 | ES9 | ECMAScript 2018 | Object spread, Promise.finally() |
| 2019 | ES10 | ECMAScript 2019 | flat(), fromEntries() |
| 2020 | ES11 | ECMAScript 2020 | BigInt, ??, ?., dynamic import |
| 2021 | ES12 | ECMAScript 2021 | replaceAll(), Promise.any() |
| 2022 | ES13 | ECMAScript 2022 | at(), private fields, top-level await |
| 2023 | ES14 | ECMAScript 2023 | Non-mutating array methods |
| 2024 | ES15 | ECMAScript 2024 | groupBy(), Promise.withResolvers() |
| 2025 | ES16 | ECMAScript 2025 | Set methods, iterator helpers |
| 2026 | ES17 | ECMAScript 2026 | Float16Array, Error.isError(), Math.sumPrecise() |
💡 Notice: There is no ES4. ES4 was a proposed major overhaul — so ambitious and controversial (classes, packages, types, interfaces, generics) that the TC39 committee could not reach agreement. After years of conflict, ES4 was abandoned in 2003. Some of its ideas resurfaced in ES6 (2015), twelve years later. This gap — and then the ten-year period from ES3 to ES5 — is why the JavaScript community was so eager for ES6 when it finally arrived.
1.3 The Three Eras of JavaScript
It helps to think of JavaScript’s history in three distinct eras:
ERA 1 — THE WILD WEST (1995–2009)
────────────────────────────────
ES1, ES2, ES3 — language is born
No standard way to handle errors, no JSON, no strict mode
Developers work around the language's inconsistencies
Cross-browser hell — IE6 vs Firefox vs Safari all behave differently
ERA 2 — STABILISATION (2009–2015)
───────────────────────────────────
ES5 brings strict mode, real JSON support, better arrays
jQuery and other libraries paper over browser differences
Node.js (2009) brings JavaScript to the server
Developers start taking JS seriously for large applications
ERA 3 — MODERN JAVASCRIPT (2015–present)
─────────────────────────────────────────
ES6/ES2015 transforms the language fundamentally
Annual releases add features steadily
TypeScript, React, Vue, Angular — all built on modern ES6+ foundations
JavaScript becomes the world's most-used programming language
2. The Story of JavaScript
2.1 Birth: Ten Days in May 1995
In May 1995, a programmer named Brendan Eich at Netscape Communications was given a task: create a scripting language for the Netscape Navigator browser. The deadline: ten days.
The result was a language originally called Mocha, then renamed LiveScript, and finally JavaScript — a marketing decision capitalising on the popularity of the Java programming language, despite the two languages being fundamentally different.
The critical design decisions made in those ten days:
- Interpreted (not compiled) — ran directly in the browser
- Loosely typed — variables could hold any type of value
- Prototype-based — objects inherit from other objects, not classes
- Functions as first-class values — functions can be stored, passed, and returned like any other value
- Event-driven — designed to respond to user actions (clicks, keypresses)
These decisions — made under extreme time pressure — shaped JavaScript forever. Some led to great expressiveness. Others led to the famous quirks (typeof null === "object", == coercion, NaN !== NaN) that developers still encounter today.
💡 Thinking Question: Brendan Eich later said he borrowed ideas from three languages: Self (prototype-based objects), Scheme (functional programming, closures), and Java (syntax). Can you identify which modern JavaScript features came from each?
2.2 The Browser Wars and Chaos (1995–1999)
JavaScript’s early years were marked by fierce competition between Netscape and Microsoft:
- 1995: Netscape releases JavaScript 1.0 in Navigator 2.0
- 1996: Microsoft creates their own version called JScript for Internet Explorer 3
- 1996: Both companies submit the language to ECMA International for standardisation (to avoid a proprietary war)
- 1997: ECMAScript 1 is published — the first official standard
- 1998: ECMAScript 2 — minor cleanup to match an ISO standard
- 1999: ECMAScript 3 — the first real feature release
The problem: even with a standard, browsers implemented it differently. JScript (IE) and JavaScript (Netscape) had subtle differences in DOM manipulation, event handling, and object behaviour. Writing code that worked in both required constant workarounds — a problem that would persist for 15+ years.
2.3 The Dark Ages: The Missing ES4 (2000–2008)
After ES3 in 1999, TC39 attempted to create ES4 — a massive overhaul. The proposal included:
- Static typing (
int,stringtypes) - Classes with real inheritance
- Packages and namespaces
- Interfaces and generics
- Many other features
The problem: The committee split. Some members (including Brendan Eich and Mozilla) wanted ES4. Others (notably Microsoft and Yahoo) felt it was too radical and would break the web. After years of arguing, the ES4 proposal was formally abandoned in 2008.
A smaller, consensus proposal called “Harmony” was agreed upon instead — which eventually became ES5 and, later, ES6.
What this meant for developers: From 1999 to 2009 — a full decade — JavaScript had no new standard features. Developers improvised, created libraries, and worked around the language’s limitations with patterns like:
- IIFE (Immediately Invoked Function Expressions) to simulate block scope
- Prototype chains for inheritance
- jQuery (2006) to paper over browser inconsistencies
- AJAX patterns for server communication without page reload
2.4 The Node.js Revolution (2009)
In 2009, Ryan Dahl released Node.js — a JavaScript runtime built on Chrome’s V8 engine that ran JavaScript on the server, outside the browser.
This was transformative:
- JavaScript developers could now write both client (browser) and server code in the same language
- The npm (Node Package Manager) ecosystem exploded — by 2024 it hosts over 2 million packages
- Companies started hiring “full-stack JavaScript” developers
- JavaScript began its march to become the most widely used programming language on Earth
2.5 ES5 Arrives (2009) — The Reliability Overhaul
The same year as Node.js, ES5 was published — the first new standard in a decade. It did not add flashy features. Instead, it fixed reliability:
- Strict mode — opt-in to safer JavaScript
JSON.parse()andJSON.stringify()— official JSON support- Array methods —
forEach,map,filter,reduce,indexOf Object.create()— better prototype-based inheritance- Property descriptors — fine-grained object property control
2.6 ES6 / ES2015 — The Modern Era Begins
After six more years of development (much of it reconciling the abandoned ES4 proposals), ES6 (officially ECMAScript 2015) was published in June 2015. It was the largest single update to JavaScript in its history:
letandconst— proper block-scoped variables- Arrow functions
() => - Classes
classsyntax - Template literals
` ` - Destructuring
{ a, b } = obj - Default parameters
function(x = 0) - Rest
...argsand Spread...arr - Promises
- Modules
import/export MapandSet- Symbols
- Iterators and generators
for...ofloopWeakMapandWeakSet
This was not just new syntax — it was a fundamentally different way of writing JavaScript. It enabled the modern frameworks (React, Vue, Angular) and changed how the language was taught.
2.7 The Annual Release Model and the Present
After ES6, TC39 adopted the annual release model (covered in the ES2016–ES2026 tutorial). JavaScript has been growing steadily and safely ever since.
Today, JavaScript:
- Runs in every web browser on earth
- Powers servers via Node.js and Deno
- Builds mobile apps via React Native
- Controls desktop apps via Electron (VS Code, Discord, Slack are all built on it)
- Powers IoT devices
- Runs machine learning models in the browser via TensorFlow.js
3. ES3 (1999) — The Foundation That Ran for a Decade
3.1 Why Study ES3?
ES3 is the version of JavaScript that shipped in Internet Explorer 6 in 2001 and formed the baseline for web development through the mid-2000s. You will encounter ES3 patterns in:
- Legacy corporate codebases
- Old WordPress themes and plugins
- jQuery source code (written to support IE)
- Any codebase that targeted IE8 or earlier
Understanding ES3 also illuminates why ES5 and ES6 made the choices they did — each feature was a direct response to an ES3 limitation.
3.2 What ES3 Introduced
ES3 (December 1999) added the core features that made JavaScript a real programming language:
Regular Expressions:
// ES3 — first official support for regex
var email = "tunde@example.com";
var pattern = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
console.log(pattern.test(email)); // true
var text = "The price is 1500 naira and 2500 naira";
var amounts = text.match(/\d+/g);
console.log(amounts); // ["1500", "2500"]
try/catch/finally — Error Handling:
// ES3 — first proper error handling
try {
var result = riskyOperation();
} catch (e) {
// e is always required in ES3 (optional catch binding came in ES2019)
alert("Error: " + e.message);
} finally {
cleanup();
}
do...while loop:
var attempts = 0;
do {
attempts++;
var result = tryConnect();
} while (!result && attempts < 3);
switch statement:
switch (day) {
case 0: console.log("Sunday"); break;
case 1: console.log("Monday"); break;
default: console.log("Weekday");
}
String methods — split(), replace(), search():
var csv = "alice,bob,carol,david";
var names = csv.split(","); // ["alice","bob","carol","david"]
var text = "Hello World";
var result = text.replace("World", "JavaScript"); // "Hello JavaScript"
3.3 ES3’s Biggest Limitations (Which ES5+ Fixed)
1. No let or const — only var:
// ES3 — var is function-scoped, not block-scoped
var x = 10;
if (true) {
var x = 20; // This OVERWRITES the outer x!
}
console.log(x); // 20 — surprising!
// ES3 workaround for block scope — IIFE (Immediately Invoked Function Expression)
var x = 10;
(function() {
var x = 20; // This x is scoped to the IIFE function
})();
console.log(x); // 10 — outer x is protected
2. No JSON support — required third-party libraries:
// ES3 — no JSON.parse() or JSON.stringify() built in!
// Developers loaded json2.js (Douglas Crockford's library)
// Dangerous workaround that was widely used (but terrible for security):
var jsonString = '{"name": "Tunde", "age": 28}';
var obj = eval("(" + jsonString + ")"); // eval! Huge security risk
3. No forEach, map, filter, reduce — only manual loops:
// ES3 — no array iteration methods
var numbers = [1, 2, 3, 4, 5];
// Had to write all processing manually with for loops
var doubled = [];
for (var i = 0; i < numbers.length; i++) {
doubled.push(numbers[i] * 2);
}
// Later added manually to Array.prototype by developers or via libraries
Array.prototype.forEach = function(callback) {
for (var i = 0; i < this.length; i++) {
callback(this[i], i, this);
}
};
4. No strict mode — silent failures everywhere:
// ES3 — this silently creates a global variable
function broken() {
x = 10; // No var, no let, no const — ES3 creates a global
}
broken();
console.log(x); // 10 — surprise global!
5. No Object.create(), Object.keys(), property descriptors:
// ES3 — simulating classical inheritance was complex and fragile
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
return this.name + " makes a sound.";
};
function Dog(name) {
Animal.call(this, name); // Manual parent constructor call
}
Dog.prototype = new Animal(); // Fragile prototype chain setup
Dog.prototype.constructor = Dog;
Dog.prototype.speak = function() {
return this.name + " barks.";
};
3.4 The IIFE Pattern — ES3’s Block Scope Workaround
Because var leaks out of blocks, ES3 developers used IIFEs (Immediately Invoked Function Expressions) to create scope boundaries.
// IIFE syntax — a function that defines and calls itself immediately
(function() {
var privateVar = "I'm hidden from the outside world";
var counter = 0;
// Everything inside here is private
function increment() {
counter++;
return counter;
}
// Expose only what needs to be public
window.myModule = {
count: increment
};
})();
console.log(typeof privateVar); // "undefined" — not accessible outside
console.log(window.myModule.count()); // 1
console.log(window.myModule.count()); // 2
This IIFE + module pattern was the backbone of JavaScript libraries in the ES3/ES5 era — including jQuery, Underscore, and Backbone. ES6 modules made it largely obsolete, but you will still encounter it in legacy code.
3.5 The arguments Object — ES3’s Rest Parameters
Before ES6 rest parameters (...args), ES3 functions had access to a special arguments object containing all the values passed in.
// ES3 — arguments object
function sum() {
var total = 0;
for (var i = 0; i < arguments.length; i++) {
total += arguments[i];
}
return total;
}
console.log(sum(1, 2, 3)); // 6
console.log(sum(10, 20, 30, 40)); // 100
// The problem: arguments is array-LIKE, not a real array
// Array methods don't work on it directly:
arguments.forEach(…); // TypeError in ES3!
// ES3 workaround — convert arguments to a real array:
var args = Array.prototype.slice.call(arguments);
args.forEach(function(arg) { console.log(arg); }); // Now works
// ES6 — rest parameters replaced this pattern cleanly
function sum(...numbers) {
return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3, 4)); // 10
// numbers is a real Array — all Array methods available
4. ES5 (2009) — The Reliability Revolution
4.1 The Context: Why ES5 Was Needed
By 2009, JavaScript was being used for applications far more complex than its original design intended. Google Maps, Gmail, and early social networks were running thousands of lines of JavaScript. The problems were becoming critical:
- Accidental global variables caused mysterious bugs
- No native JSON parsing was a security and performance nightmare
- Working with object properties was primitive
- No standard way to protect object properties from modification
- Arrays lacked the functional methods that made code clean
ES5 addressed all of these without breaking existing code — everything in ES5 is backward-compatible with ES3.
4.2 "use strict" — Opt-In to Safer JavaScript
Strict mode is applied by adding the string "use strict" at the top of a file or function. It activates a stricter interpretation of the code and converts many silent errors into thrown exceptions.
"use strict"; // File-level strict mode
// Now these cause errors instead of silent failures:
x = 10; // ReferenceError: x is not defined (no var/let/const)
delete x; // SyntaxError: can't delete a variable
var obj = {};
Object.freeze(obj);
obj.newProp = "test"; // TypeError: cannot add to frozen object
// Function-level strict mode:
function strictFunction() {
"use strict";
y = 5; // ReferenceError — only affects this function
}
function normalFunction() {
z = 5; // Still creates a global in non-strict mode
}
What strict mode prevents:
| Bad Code | ES3 Behaviour | Strict Mode Behaviour |
|---|---|---|
x = 5 (no declaration) |
Creates global variable | ReferenceError |
delete varName |
Silently fails | SyntaxError |
| Duplicate param names | Allowed | SyntaxError |
with statement |
Works (confusingly) | SyntaxError |
this in functions |
Is window |
Is undefined |
| Writing to read-only property | Silently fails | TypeError |
Octal literals 0777 |
Allowed | SyntaxError |
💡 Important context: ES6 modules are automatically in strict mode. Class bodies are also automatically in strict mode. So in modern JavaScript, you rarely need to write
"use strict"explicitly — but understanding it is essential for reading older code and understanding why the language behaves the way it does.
4.3 JSON.parse() and JSON.stringify() — Native JSON Support
Before ES5, JSON (JavaScript Object Notation) had no native browser support. Developers either used eval() (dangerous) or loaded Crockford’s json2.js library.
ES5 built JSON handling directly into the language.
JSON.stringify() — JavaScript value → JSON string:
// Simple object
let user = { name: "Tunde", age: 28, city: "Lagos" };
let json = JSON.stringify(user);
console.log(json);
// '{"name":"Tunde","age":28,"city":"Lagos"}'
console.log(typeof json); // "string"
// Pretty-print with indentation (third argument)
let pretty = JSON.stringify(user, null, 2);
console.log(pretty);
// {
// "name": "Tunde",
// "age": 28,
// "city": "Lagos"
// }
// Array
let scores = [85, 92, 78];
console.log(JSON.stringify(scores)); // "[85,92,78]"
// Nested object
let order = {
id: 101,
customer: { name: "Tunde", email: "t@example.com" },
items: ["Laptop", "Mouse"],
total: 462000
};
console.log(JSON.stringify(order, null, 2));
What JSON.stringify() cannot handle:
let obj = {
name: "Tunde",
fn: function() {}, // Functions → omitted
undef: undefined, // undefined → omitted
sym: Symbol("id"), // Symbols → omitted
circular: null // Circular references → TypeError
};
console.log(JSON.stringify(obj));
// '{"name":"Tunde"}' ← only serialisable properties remain
JSON.parse() — JSON string → JavaScript value:
let jsonString = '{"name":"Tunde","age":28,"scores":[85,92,78]}';
let parsed = JSON.parse(jsonString);
console.log(parsed.name); // "Tunde"
console.log(parsed.scores[1]); // 92
console.log(typeof parsed); // "object"
// With a reviver function — transform values during parsing
let withDates = '{"name":"Tunde","joined":"2024-01-15"}';
let result = JSON.parse(withDates, (key, value) => {
if (key === "joined") return new Date(value); // Convert string to Date
return value;
});
console.log(result.joined instanceof Date); // true
console.log(result.joined.getFullYear()); // 2024
Real-world use — API communication:
// Every API call uses these methods
async function saveUser(user) {
let response = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(user) // ← ES5 JSON.stringify
});
return JSON.parse(await response.text()); // ← ES5 JSON.parse
// (or response.json() which calls JSON.parse internally)
}
4.4 ES5 Array Methods — The Functional Revolution
Before ES5, processing arrays required manual for loops. ES5 added ten methods that made array processing declarative and expressive.
Array.forEach() — Iterate with a callback:
// ES3 — manual loop
var names = ["Alice", "Bob", "Carol"];
for (var i = 0; i < names.length; i++) {
console.log(names[i]);
}
// ES5 — forEach
names.forEach(function(name, index) {
console.log(index + ": " + name);
});
// 0: Alice 1: Bob 2: Carol
Array.map() — Transform each element:
var prices = [100, 250, 80, 310];
// Increase all prices by 10%
var increased = prices.map(function(price) {
return price * 1.1;
});
console.log(increased); // [110, 275, 88, 341]
// Note: map() does NOT modify the original array
console.log(prices); // [100, 250, 80, 310] — unchanged
Array.filter() — Keep matching elements:
var products = [
{ name: "Laptop", price: 450000, inStock: true },
{ name: "Mouse", price: 8000, inStock: true },
{ name: "Monitor", price: 180000, inStock: false },
{ name: "Keyboard", price: 12000, inStock: true }
];
var affordable = products.filter(function(p) {
return p.price <= 15000 && p.inStock;
});
console.log(affordable.map(function(p) { return p.name; }));
// ["Mouse", "Keyboard"]
Array.reduce() — Accumulate to a single value:
var cart = [
{ name: "Laptop", price: 450000, qty: 1 },
{ name: "Mouse", price: 8000, qty: 2 },
{ name: "Keyboard", price: 12000, qty: 1 }
];
var total = cart.reduce(function(accumulator, item) {
return accumulator + (item.price * item.qty);
}, 0); // 0 is the initial value of accumulator
console.log("Total: ₦" + total.toLocaleString()); // Total: ₦478,000
Array.indexOf() — Find element position:
var fruits = ["mango", "pear", "pineapple", "mango"];
console.log(fruits.indexOf("pear")); // 1
console.log(fruits.indexOf("mango")); // 0 — first occurrence
console.log(fruits.indexOf("guava")); // -1 — not found
console.log(fruits.lastIndexOf("mango")); // 3 — last occurrence
Array.every() and Array.some():
var scores = [75, 82, 90, 68, 88];
// every() — true only if ALL elements pass
var allPassed = scores.every(function(score) { return score >= 60; });
console.log(allPassed); // true — all scores are 60 or above
// some() — true if AT LEAST ONE element passes
var anyExcellent = scores.some(function(score) { return score >= 90; });
console.log(anyExcellent); // true — 90 exists in the array
Array.isArray() — Reliably check for arrays:
// Before ES5 — typeof [] returns "object", making it hard to check
console.log(typeof []); // "object" — not helpful!
console.log(typeof {}); // "object" — same as array!
// ES5 — Array.isArray() is definitive
console.log(Array.isArray([])); // true
console.log(Array.isArray({})); // false
console.log(Array.isArray("hi")); // false
Array.indexOf() for strings:
var text = "Hello, JavaScript World!";
console.log(text.indexOf("JavaScript")); // 7 — position of first char
console.log(text.indexOf("Python")); // -1 — not found
4.5 Object.create() — Cleaner Prototype-Based Inheritance
Object.create(proto) creates a new object whose prototype is the specified object. This enables clean prototypal inheritance without new constructors.
// ES3 prototype inheritance — clunky
function Animal(name) {
this.name = name;
}
Animal.prototype.speak = function() {
return this.name + " makes a sound.";
};
// ES5 Object.create — cleaner prototypal inheritance
var animalPrototype = {
speak: function() {
return this.name + " makes a sound.";
},
eat: function() {
return this.name + " is eating.";
}
};
var dog = Object.create(animalPrototype);
dog.name = "Rex";
console.log(dog.speak()); // "Rex makes a sound."
console.log(Object.getPrototypeOf(dog) === animalPrototype); // true
// Create with null prototype — no inherited properties
var pureObj = Object.create(null);
pureObj.key = "value";
// pureObj has NO toString, hasOwnProperty, etc. — truly bare
4.6 Property Descriptors — Fine-Grained Object Control
ES5 introduced the ability to define objects with controlled properties — making some read-only, non-enumerable, or non-configurable.
Object.defineProperty() — Define or modify a single property:
var user = {};
Object.defineProperty(user, "id", {
value: 42,
writable: false, // Cannot be changed after set
enumerable: false, // Does not appear in for...in or Object.keys()
configurable: false // Cannot be redefined or deleted
});
console.log(user.id); // 42
user.id = 99; // Silently fails (or TypeError in strict mode)
console.log(user.id); // 42 — unchanged
console.log(Object.keys(user)); // [] — id is non-enumerable
// Getters and setters with defineProperty
var temperature = {};
var _celsius = 0;
Object.defineProperty(temperature, "celsius", {
get: function() { return _celsius; },
set: function(value) {
if (value < -273.15) throw new RangeError("Temperature below absolute zero!");
_celsius = value;
},
enumerable: true,
configurable: true
});
temperature.celsius = 25;
console.log(temperature.celsius); // 25
temperature.celsius = -300; // RangeError: Temperature below absolute zero!
Object.freeze() — Make an object immutable:
var config = Object.freeze({
apiUrl: "https://api.example.com",
timeout: 3000,
retries: 3
});
config.apiUrl = "https://attacker.com"; // Silently fails (TypeError in strict mode)
config.newProp = "hacked"; // Also fails
console.log(config.apiUrl); // "https://api.example.com" — unchanged
console.log(Object.isFrozen(config)); // true
💡 Note:
Object.freeze()is shallow — only the top level is frozen. Nested objects can still be modified. For deep freezing, you need a recursive function.
Object.keys() — Get an array of enumerable own property names:
var car = {
brand: "Toyota",
model: "Camry",
year: 2023
};
console.log(Object.keys(car)); // ["brand", "model", "year"]
// Iterate over an object's properties (ES5 style)
Object.keys(car).forEach(function(key) {
console.log(key + ": " + car[key]);
});
// brand: Toyota model: Camry year: 2023
Object.seal() — Allow updates, prevent structure changes:
// Object.seal() prevents adding or deleting properties,
// but existing properties can still be changed
var settings = Object.seal({
theme: "light",
lang: "en"
});
settings.theme = "dark"; // ✅ Allowed — modifying existing property
settings.font = "Arial"; // ❌ Silently fails — adding new property
delete settings.lang; // ❌ Silently fails — deleting property
console.log(settings); // { theme: "dark", lang: "en" }
4.7 ES5 Function.bind() — Fix the this Problem
One of JavaScript’s most confusing behaviours was the unpredictable value of this inside functions. bind() permanently attaches a this context to a function.
var user = {
name: "Tunde",
greet: function() {
return "Hello, I am " + this.name;
}
};
// The this problem:
var greetFn = user.greet;
console.log(greetFn()); // "Hello, I am undefined" — this is window/global, not user
// ES5 bind() — fix this permanently
var boundGreet = user.greet.bind(user);
console.log(boundGreet()); // "Hello, I am Tunde" ✅
// bind() with pre-set arguments (partial application)
function multiply(a, b) {
return a * b;
}
var double = multiply.bind(null, 2); // Pre-set first argument to 2
console.log(double(5)); // 10
console.log(double(8)); // 16
console.log(double(15)); // 30
Real-world use — event handlers in ES5 OOP:
function Counter() {
this.count = 0;
this.increment = this.increment.bind(this); // Fix `this` for event use
}
Counter.prototype.increment = function() {
this.count++;
console.log("Count:", this.count);
};
var counter = new Counter();
document.getElementById("btn").addEventListener("click", counter.increment);
// Without .bind(this), `this` inside increment would be the button element!
4.8 ES5 String and Date Improvements
String.trim():
var input = " Hello World ";
console.log(input.trim()); // "Hello World"
// Essential for processing form input
function processFormInput(raw) {
return raw.trim();
}
Date.now():
// ES5 — simpler way to get current timestamp in milliseconds
var timestamp = Date.now();
console.log(timestamp); // e.g., 1700000000000
// Before ES5:
var old = new Date().getTime(); // Same result, more verbose
4.9 ES5 Feature Summary
| Feature | What It Does |
|---|---|
"use strict" |
Strict mode — catches silent errors as exceptions |
JSON.parse() |
JSON string → JavaScript value |
JSON.stringify() |
JavaScript value → JSON string |
Array.forEach() |
Execute callback for each element |
Array.map() |
Transform array elements |
Array.filter() |
Keep matching elements |
Array.reduce() |
Accumulate to a single value |
Array.indexOf() |
Find element position |
Array.every() |
True if all elements match |
Array.some() |
True if any element matches |
Array.isArray() |
Reliably detect arrays |
Object.create() |
Create object with specified prototype |
Object.keys() |
Array of own enumerable property names |
Object.defineProperty() |
Fine-grained property control |
Object.freeze() |
Make object immutable |
Object.seal() |
Allow updates, prevent structural changes |
Function.bind() |
Fix this context permanently |
String.trim() |
Remove surrounding whitespace |
Date.now() |
Current timestamp in milliseconds |
5. ES6 / ES2015 — The Modern Language Transformation
5.1 Why ES6 Was the Biggest Release Ever
ES6 (June 2015) was the result of six years of work reconciling the failed ES4 proposal with what the web actually needed. It contained more new features than all previous versions combined. It didn’t just add to JavaScript — it fundamentally changed how JavaScript is written and taught.
Every modern JavaScript tutorial, every React component, every Node.js server uses ES6+ syntax. If ES5 was JavaScript “growing up,” ES6 was JavaScript becoming a modern, professional-grade language.
5.2 let and const — Fixing Variable Scope
let and const provide block-level scoping — the behaviour developers expected from day one but didn’t get with var.
// var — function scoped, hoisted, re-declarable (the source of many bugs)
function varDemo() {
console.log(x); // undefined — hoisted!
var x = 10;
if (true) {
var x = 20; // Re-declares and overwrites the outer x
console.log(x); // 20
}
console.log(x); // 20 — the if-block's x leaked out!
}
// let — block scoped, not hoisted to usable state, not re-declarable
function letDemo() {
// console.log(y); // ReferenceError — Temporal Dead Zone
let y = 10;
if (true) {
let y = 20; // New y, scoped to this block only
console.log(y); // 20
}
console.log(y); // 10 — outer y unchanged ✅
}
// const — block scoped, binding cannot be reassigned
const PI = 3.14159;
PI = 3; // TypeError: Assignment to constant variable
const config = { theme: "dark" };
config.theme = "light"; // ✅ Object contents can change
config = {}; // ❌ TypeError — the binding cannot change
Rule of thumb (modern JS):
- Use
constby default - Upgrade to
letonly when you need to reassign - Never use
varin new code
5.3 Arrow Functions => — Concise and Lexically Scoped this
Arrow functions provide a shorter syntax and — critically — inherit this from their enclosing scope rather than having their own this context.
// ES5 function
var double = function(x) { return x * 2; };
// ES6 arrow function
const double2 = x => x * 2;
// Multiple parameters require parentheses
const add = (a, b) => a + b;
// Multi-line body requires braces and explicit return
const processUser = (user) => {
const name = user.name.toUpperCase();
const email = user.email.toLowerCase();
return { name, email };
};
// No parameters — empty parentheses
const getTimestamp = () => Date.now();
The critical this difference:
// ES5 — `this` problem in callbacks
function Timer() {
this.seconds = 0;
var self = this; // Classic ES5 workaround — save `this` reference
setInterval(function() {
self.seconds++; // Must use `self` because `this` here is window
console.log(self.seconds);
}, 1000);
}
// ES6 — arrow function inherits `this` from Timer
function Timer2() {
this.seconds = 0;
setInterval(() => {
this.seconds++; // `this` is the Timer2 instance ✅
console.log(this.seconds);
}, 1000);
}
Arrow functions in array methods:
const products = [
{ name: "Laptop", price: 450000 },
{ name: "Mouse", price: 8000 },
{ name: "Keyboard", price: 12000 }
];
// ES5
var names = products.map(function(p) { return p.name; });
var cheap = products.filter(function(p) { return p.price < 15000; });
// ES6
const names2 = products.map(p => p.name);
const cheap2 = products.filter(p => p.price < 15000);
const total = products.reduce((sum, p) => sum + p.price, 0);
console.log(names2); // ["Laptop", "Mouse", "Keyboard"]
console.log(cheap2.map(p => p.name)); // ["Mouse", "Keyboard"]
console.log(total); // 470000
5.4 Template Literals — String Interpolation
Template literals (backtick strings) allow embedded expressions and multi-line strings.
const name = "Tunde";
const balance = 50000;
const date = "2024-01-15";
// ES5 concatenation — error-prone and hard to read
var msg1 = "Dear " + name + ",\nYour balance as of " + date + " is ₦" + balance.toLocaleString() + ".";
// ES6 template literal — clean, readable, multi-line
const msg2 = `Dear ${name},
Your balance as of ${date} is ₦${balance.toLocaleString()}.`;
console.log(msg2);
// Dear Tunde,
// Your balance as of 2024-01-15 is ₦50,000.
// Expressions inside ${}
const a = 10, b = 3;
console.log(`${a} + ${b} = ${a + b}`); // 10 + 3 = 13
console.log(`${a} > ${b}: ${a > b}`); // 10 > 3: true
console.log(`${name.toUpperCase()}`); // TUNDE
Tagged template literals — advanced:
// A tag function receives the template parts and values separately
function highlight(strings, ...values) {
return strings.reduce((result, str, i) => {
return result + str + (values[i] !== undefined ? `<b>${values[i]}</b>` : "");
}, "");
}
const product = "Laptop";
const price = 450000;
const html = highlight`Item: ${product}, Price: ₦${price.toLocaleString()}`;
console.log(html);
// Item: <b>Laptop</b>, Price: ₦<b>450,000</b>
5.5 Destructuring — Extract Values Cleanly
Array destructuring:
// ES5
var arr = [10, 20, 30];
var first = arr[0];
var second = arr[1];
var third = arr[2];
// ES6 — array destructuring
const [first2, second2, third2] = [10, 20, 30];
console.log(first2, second2, third2); // 10 20 30
// Skip elements
const [,, thirdOnly] = [10, 20, 30];
console.log(thirdOnly); // 30
// Default values
const [a = 0, b = 0, c = 0, d = 0] = [10, 20];
console.log(a, b, c, d); // 10 20 0 0
// Swap variables without temp
let x = 5, y = 10;
[x, y] = [y, x];
console.log(x, y); // 10 5
// From function return
function getCoordinates() { return [51.5074, -0.1278]; }
const [latitude, longitude] = getCoordinates();
Object destructuring:
const user = {
id: 1,
name: "Tunde",
email: "tunde@example.com",
role: "admin",
address: { city: "Lagos", country: "Nigeria" }
};
// ES5
var userName = user.name;
var userRole = user.role;
// ES6 — object destructuring
const { name, role } = user;
console.log(name, role); // "Tunde" "admin"
// Rename while destructuring
const { name: displayName, email: contactEmail } = user;
console.log(displayName); // "Tunde"
console.log(contactEmail); // "tunde@example.com"
// Default values
const { name: n, department = "Engineering" } = user;
console.log(department); // "Engineering" — default because user has no department
// Nested destructuring
const { address: { city, country } } = user;
console.log(city, country); // "Lagos" "Nigeria"
// In function parameters
function displayUser({ name, role, address: { city } = {} }) {
console.log(`${name} (${role}) — ${city}`);
}
displayUser(user); // "Tunde (admin) — Lagos"
5.6 Default Parameters
// ES5 — manual default handling
function greet(name, greeting) {
name = name || "Guest";
greeting = greeting || "Hello";
return greeting + ", " + name + "!";
}
// ES6 — default parameters
function greet2(name = "Guest", greeting = "Hello") {
return `${greeting}, ${name}!`;
}
console.log(greet2()); // "Hello, Guest!"
console.log(greet2("Tunde")); // "Hello, Tunde!"
console.log(greet2("Tunde", "Welcome")); // "Welcome, Tunde!"
// Default can reference earlier parameters
function createRange(start = 0, end = start + 10) {
return { start, end };
}
console.log(createRange()); // { start: 0, end: 10 }
console.log(createRange(5)); // { start: 5, end: 15 }
console.log(createRange(5, 100)); // { start: 5, end: 100 }
5.7 Rest and Spread Operators
Rest ... — collect remaining arguments/elements:
// Rest in function parameters
function sum(first, second, ...rest) {
console.log("First:", first);
console.log("Second:", second);
console.log("Rest:", rest); // Real array — can use all Array methods!
return first + second + rest.reduce((a, b) => a + b, 0);
}
console.log(sum(1, 2, 3, 4, 5)); // First:1 Second:2 Rest:[3,4,5] → 15
// Rest in destructuring
const [head, ...tail] = [1, 2, 3, 4, 5];
console.log(head); // 1
console.log(tail); // [2, 3, 4, 5]
const { name, ...details } = { name: "Tunde", age: 28, city: "Lagos" };
console.log(name); // "Tunde"
console.log(details); // { age: 28, city: "Lagos" }
Spread ... — expand iterable into individual elements:
// Spread in function calls
const numbers = [3, 1, 4, 1, 5, 9, 2, 6];
console.log(Math.max(...numbers)); // 9 — spread as individual arguments
// Copy and merge arrays
const a = [1, 2, 3];
const b = [4, 5, 6];
const merged = [...a, ...b]; // [1, 2, 3, 4, 5, 6]
const copy = [...a]; // [1, 2, 3] — independent copy
// Copy and merge objects
const defaults = { theme: "light", lang: "en" };
const overrides = { theme: "dark" };
const config = { ...defaults, ...overrides }; // { theme: "dark", lang: "en" }
5.8 Classes — Cleaner OOP Syntax
ES6 classes provide clean syntax over JavaScript’s prototype system. They are syntactic sugar — under the hood, prototypes still do the work.
// ES5 — prototype-based OOP (verbose and fragile)
function Animal(name, sound) {
this.name = name;
this.sound = sound;
}
Animal.prototype.speak = function() {
return this.name + " says " + this.sound;
};
// ES6 — class syntax (clean and familiar)
class Animal {
constructor(name, sound) {
this.name = name;
this.sound = sound;
}
speak() {
return `${this.name} says ${this.sound}`;
}
static kingdom() {
return "Animalia"; // Static method — called on class, not instance
}
}
// Inheritance with extends
class Dog extends Animal {
constructor(name) {
super(name, "woof"); // Call parent constructor
}
fetch(item) {
return `${this.name} fetches the ${item}!`;
}
}
const rex = new Dog("Rex");
console.log(rex.speak()); // "Rex says woof"
console.log(rex.fetch("ball")); // "Rex fetches the ball!"
console.log(Animal.kingdom()); // "Animalia"
console.log(rex instanceof Dog); // true
console.log(rex instanceof Animal); // true — inheritance chain works
5.9 Promises — Structured Asynchronous Code
Promises represent a value that may be available now, later, or never. They replaced callback hell with a chainable pattern.
// A Promise has three states: pending → fulfilled OR rejected
function fetchData(url) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (url.startsWith("https")) {
resolve({ data: "Success!", url });
} else {
reject(new Error("Only HTTPS URLs supported"));
}
}, 200);
});
}
// Using a Promise
fetchData("https://api.example.com")
.then(result => {
console.log("Data:", result.data);
return result.url; // Return passes value to next .then()
})
.then(url => console.log("URL:", url))
.catch(error => console.error("Error:", error.message))
.finally(() => console.log("Request complete"));
// Chaining avoids callback pyramid:
fetchUser(1)
.then(user => fetchOrders(user.id))
.then(orders => fetchShipping(orders[0].id))
.then(ship => console.log("Status:", ship.status))
.catch(err => console.error(err));
5.10 Modules — Official Import/Export
ES6 modules allow splitting code across files with a clear interface contract.
// math.js — named exports
export const PI = 3.14159;
export function area(r) { return PI * r * r; }
export function circumference(r) { return 2 * PI * r; }
// utils.js — default export
export default function formatCurrency(amount, currency = "NGN") {
return `${currency} ${amount.toLocaleString()}`;
}
// app.js — importing
import formatCurrency from "./utils.js"; // Default import
import { PI, area } from "./math.js"; // Named imports
import { area as circleArea } from "./math.js"; // Rename on import
import * as MathUtils from "./math.js"; // Import everything as namespace
console.log(PI); // 3.14159
console.log(area(5)); // 78.53975
console.log(MathUtils.circumference(5)); // 31.4159
console.log(formatCurrency(50000)); // "NGN 50,000"
5.11 Map and Set — Purpose-Built Data Structures
// Map — key-value pairs with ANY key type
const userSessions = new Map();
const user1 = { id: 1, name: "Tunde" };
const user2 = { id: 2, name: "Alice" };
userSessions.set(user1, { loginTime: Date.now(), page: "/dashboard" });
userSessions.set(user2, { loginTime: Date.now(), page: "/settings" });
console.log(userSessions.get(user1).page); // "/dashboard"
console.log(userSessions.size); // 2
// Map iteration
for (const [user, session] of userSessions) {
console.log(`${user.name}: ${session.page}`);
}
// Set — unique values only
const tags = new Set(["javascript", "react", "javascript", "nodejs", "react"]);
console.log(tags); // Set(3) {"javascript", "react", "nodejs"} — duplicates removed
console.log(tags.size); // 3
tags.add("typescript");
tags.delete("react");
// Convert to array
const tagArray = [...tags];
console.log(tagArray); // ["javascript", "nodejs", "typescript"]
// Classic use: remove duplicates from an array
const ids = [1, 2, 3, 2, 1, 4, 3, 5];
const uniqueIds = [...new Set(ids)];
console.log(uniqueIds); // [1, 2, 3, 4, 5]
5.12 Symbols — Unique Identifiers
Symbol creates a guaranteed-unique value. No two symbols are ever equal, even if they have the same description.
const id1 = Symbol("id");
const id2 = Symbol("id");
console.log(id1 === id2); // false — always unique
console.log(typeof id1); // "symbol"
console.log(id1.toString()); // "Symbol(id)"
console.log(id1.description); // "id"
// Primary use: unique object keys that don't conflict
const USER_ID = Symbol("userId");
const user = {
name: "Tunde",
[USER_ID]: 42 // Symbol as computed key
};
console.log(user[USER_ID]); // 42
console.log(user.name); // "Tunde"
// Symbols don't appear in for...in or JSON.stringify
for (let key in user) {
console.log(key); // Only "name" — USER_ID is hidden
}
console.log(JSON.stringify(user)); // '{"name":"Tunde"}' — Symbol omitted
// Well-known Symbols — used to customise object behaviour
class Range {
constructor(start, end) {
this.start = start;
this.end = end;
}
[Symbol.iterator]() { // Make Range iterable with for...of
let current = this.start;
const end = this.end;
return {
next() {
return current <= end
? { value: current++, done: false }
: { value: undefined, done: true };
}
};
}
}
for (let n of new Range(1, 5)) {
console.log(n); // 1 2 3 4 5
}
5.13 Generators — Pausable Functions
Generator functions (function*) can be paused mid-execution and resumed, yielding values one at a time.
function* countUp(start = 0) {
while (true) {
yield start++;
}
}
const counter = countUp(1);
console.log(counter.next().value); // 1
console.log(counter.next().value); // 2
console.log(counter.next().value); // 3
// Finite generator
function* fibonacci() {
let a = 0, b = 1;
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
const fib = fibonacci();
const first10 = Array.from({ length: 10 }, () => fib.next().value);
console.log(first10);
// [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
5.14 ES6 Feature Summary
| Feature | Replaces / Improves |
|---|---|
let / const |
var — block scoping |
Arrow functions => |
function — concise + lexical this |
Template literals ` |
String concatenation |
| Destructuring | Manual property/index access |
| Default parameters | param = param || default patterns |
Rest ...args |
arguments object |
Spread ...arr |
apply() and concat() patterns |
class syntax |
Constructor functions + prototype chains |
| Promises | Callback-based async patterns |
import/export |
IIFE module patterns, script loading order |
Map/Set |
Plain objects used as maps; manual deduplication |
Symbol |
String-keyed properties that might collide |
| Generators | Manual iterator implementations |
for...of |
Manual loops over iterables |
WeakMap/WeakSet |
Memory-leaking associations |
6. IE and Edge — The Browser Compatibility Story
6.1 Internet Explorer: The Villain of Web Development
Internet Explorer (IE) was Microsoft’s web browser from 1995 to 2022. At its peak in 2003, IE had over 95% market share. For years, “write JavaScript” and “make IE work” were inseparable challenges.
Why IE was so difficult:
- Microsoft had their own implementation called JScript that diverged from the standard
- IE was notoriously slow to adopt new ECMAScript standards
- Each version of IE had its own bugs, inconsistencies, and proprietary features
- The browser was bundled with Windows, so users didn’t update even when better options existed
- Corporate environments locked to IE6 or IE8 persisted for over a decade after those versions were obsolete
6.2 The IE Version Timeline and JavaScript Support
| IE Version | Released | Key JS Limitations |
|---|---|---|
| IE 6 | 2001 | ES3 only. Rampant bugs. ActiveX. |
| IE 7 | 2006 | Still ES3. Minor fixes. No canvas. |
| IE 8 | 2009 | ES3 only. Partial ES5. No JSON natively. No canvas. |
| IE 9 | 2011 | First canvas support. Still limited ES5. |
| IE 10 | 2012 | Better ES5 support. WebSockets. |
| IE 11 | 2013 | Full ES5. Partial ES6 (no arrow functions, no let/const). |
IE 11’s ES6 support gaps (the developer nightmare):
IE 11 was the last version of IE and the one developers had to support the longest. Even though ES6 was published in 2015, IE 11 did not support:
letandconst(partially, with bugs)- Arrow functions
=> - Template literals
- Destructuring
- Default parameters
- Rest/spread operators
classsyntax- Promises
Map/Set(partially)import/exportmodules- Generator functions
This is exactly why tools like Babel and webpack became essential — they transpile modern ES6+ code into IE-compatible ES5.
6.3 Microsoft Edge — The Replacement
In 2015, Microsoft released Microsoft Edge as a replacement for IE. The original Edge used Microsoft’s own rendering engine (“EdgeHTML”). In 2019, Microsoft made a major decision: rebuild Edge on the Chromium engine (the same open-source engine that powers Chrome).
| Edge Version | Engine | ES6 Support |
|---|---|---|
| Edge (Legacy, 2015–2019) | EdgeHTML | Partial ES6 |
| Edge (Chromium, 2020+) | Blink/V8 | Full modern JS |
The retirement of IE:
- June 15, 2022: Microsoft officially ended support for IE 11 on Windows 10
- IE was removed from Windows 11 entirely
- Enterprise users were migrated to Edge with IE Mode for legacy apps
6.4 Browser Feature Detection vs Browser Detection
The old way to handle browser differences was browser detection — checking which browser the user had and writing different code paths:
// ❌ Browser detection — fragile and bad practice
if (navigator.userAgent.indexOf("MSIE") !== -1) {
// IE-specific code
var xhr = new ActiveXObject("Microsoft.XMLHTTP");
} else {
// Modern browser code
var xhr = new XMLHttpRequest();
}
Problems with this approach:
- User agent strings can be faked
- New browser versions break the detection logic
- You’re assuming what features a browser has based on its name — wrong
The modern approach is feature detection — checking whether the specific feature you need actually exists:
// ✅ Feature detection — checks for capability, not identity
if (typeof window.fetch === "function") {
// fetch API is available — use it
fetch("/api/data").then(r => r.json()).then(console.log);
} else {
// fetch not available — use XMLHttpRequest fallback
var xhr = new XMLHttpRequest();
xhr.open("GET", "/api/data");
xhr.onload = function() { console.log(JSON.parse(xhr.responseText)); };
xhr.send();
}
// Feature detect ES6 features
if (typeof Symbol === "function") {
console.log("ES6 Symbols supported");
}
if (typeof Promise === "function") {
console.log("Promises supported");
}
6.5 Polyfills — Backfilling Missing Features
A polyfill is code that implements a modern feature for older browsers that don’t have it natively. The name comes from “Polyfilla” — a British wall-filler product that smooths over cracks and holes.
// Polyfill for Array.from() — not in IE
if (!Array.from) {
Array.from = function(arrayLike) {
return Array.prototype.slice.call(arrayLike);
};
}
// Polyfill for String.prototype.includes()
if (!String.prototype.includes) {
String.prototype.includes = function(search, start) {
return this.indexOf(search, start) !== -1;
};
}
// Polyfill for Promise (simplified — real polyfills are much more complex)
// Libraries like `promise-polyfill` provide full implementations
if (typeof Promise === "undefined") {
// Load a Promise polyfill
}
Popular polyfill libraries:
- core-js — the most comprehensive JavaScript polyfill library
- Babel polyfill — uses core-js under the hood
- es6-promise — Promise-only polyfill
6.6 Transpilation with Babel — Write Modern, Deploy Everywhere
Babel is a JavaScript compiler (transpiler) that converts modern JavaScript (ES6+) into older JavaScript (ES5) that runs in older browsers.
// ES6 code you write:
const greet = (name = "Guest") => `Hello, ${name}!`;
// What Babel transpiles it to (ES5 output):
"use strict";
var greet = function greet() {
var name = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : "Guest";
return "Hello, " + name + "!";
};
// ES6 class
class Animal {
constructor(name) { this.name = name; }
speak() { return `${this.name} speaks`; }
}
// Babel output (ES5):
"use strict";
function _classCallCheck(instance, Constructor) { /* ... */ }
var Animal = function() {
function Animal(name) {
_classCallCheck(this, Animal);
this.name = name;
}
Animal.prototype.speak = function speak() {
return this.name + " speaks";
};
return Animal;
}();
How Babel fits in a modern project:
Your Code (ES6+)
↓
Babel (transpiler)
↓
Bundler (webpack/Vite)
↓
Polyfills (core-js)
↓
Minifier (terser)
↓
Output: bundle.js
(ES5 compatible, minified)
↓
Browser
6.7 Can I Use? — Checking Browser Support
The website caniuse.com shows which browsers support which features. Use it whenever you’re unsure if a feature is safe to use.
Example queries:
- “CSS Grid” → shows which IE versions don’t support it
- “Optional chaining” → shows it arrived in Chrome 80, Firefox 74, Safari 13.1, Edge 80
- “fetch API” → shows it’s not in IE at all
JavaScript-specific support tables:
- MDN Web Docs → every feature page has a “Browser compatibility” table at the bottom
- node.green → shows which Node.js versions support which ES features
- kangax.github.io/compat-table → detailed ES5/ES6/ES2016+ support tables
6.8 The Modern Browser Landscape
As of 2024–2025, IE is gone. The modern browser landscape:
| Browser | Engine | ES Version Support |
|---|---|---|
| Chrome 120+ | V8 | ES2024+ |
| Edge 120+ | V8 (Chromium-based) | ES2024+ |
| Firefox 120+ | SpiderMonkey | ES2024+ |
| Safari 17+ | JavaScriptCore | ES2023+ |
| Node.js 22+ | V8 | ES2024+ |
| Deno 1.40+ | V8 | ES2024+ |
Practical implication for new projects: If you are building for modern browsers (and most new projects should be), you can use ES2020+ features without any transpilation. Babel and polyfills are now primarily needed only for:
- Supporting older Safari versions
- Using very bleeding-edge features (Stage 3 proposals)
- Building libraries that need maximum compatibility
PHASE 2 — APPLIED EXERCISES
Exercise 1 — The ES3 → ES5 → ES6 Rewrite
Objective: Rewrite the same function three times — once in ES3 style, once in ES5, once in ES6 — observing how the language improved.
The task: A function that takes an array of order objects, filters to orders over ₦10,000, calculates a 5% tax on each, and returns the items sorted by total (including tax) ascending.
ES3 Version (write this first):
// Write using: var, for loops, manual array building, no JSON
// Expected output: [{name:"Mouse",total:12600}, {name:"Keyboard",total:15750}]
// (using sample data below)
var orders = [
{ name: "Pen", price: 500 },
{ name: "Notebook", price: 1200 },
{ name: "Mouse", price: 12000},
{ name: "Keyboard", price: 15000}
];
// Your ES3 code here — use only: var, for, if, push, sort, manual math
ES5 Version:
// Rewrite using: var/function, forEach/filter/map/reduce/sort, JSON.stringify
// No arrow functions, no template literals
ES6 Version:
// Rewrite using: const/let, arrow functions, destructuring,
// template literals, method chaining
Step-by-step instructions:
- Write the ES3 version first — no shortcuts allowed
- Refactor to ES5 using array methods (no manual for loops)
- Refactor to ES6 (full modern syntax)
- Compare the three versions side-by-side — count the lines and note readability
- Identify which ES3 version features needed the most rewriting
Exercise 2 — Version Detective
Objective: Look at the following code snippets and identify which ES version (ES3, ES5, or ES6+) they were written for, based on the features used.
// Snippet A
var total = 0;
for (var i = 0; i < items.length; i++) {
total += items[i].price;
}
var discount = total > 50000 ? total * 0.1 : 0;
// Snippet B
"use strict";
var filtered = products.filter(function(p) {
return p.inStock && p.price >= 1000;
});
var names = filtered.map(function(p) { return p.name; });
console.log(JSON.stringify(names));
// Snippet C
const { name, price, category = "General" } = product;
const tax = price * 0.075;
const summary = `${name} (${category}): ₦${(price + tax).toLocaleString()}`;
// Snippet D
(function() {
var privateData = {};
window.DataStore = {
set: function(key, val) { privateData[key] = val; },
get: function(key) { return privateData[key]; }
};
})();
// Snippet E
const processOrders = async (userId) => {
const { orders, ...meta } = await fetchUserData(userId);
return orders
.filter(o => o.status !== "cancelled")
.flatMap(o => o.items)
.toSorted((a, b) => b.price - a.price);
};
For each snippet:
- Identify the ES version(s) it targets
- List the specific features that make you certain
- Identify any features that could NOT run in ES3
- Write the oldest-compatible rewrite for snippet C (make it ES3 compatible)
Exercise 3 — Polyfill Writing
Objective: Implement ES5 methods from scratch to understand how they work internally.
Task — implement these three methods:
// 1. Array.prototype.myMap — behaves exactly like Array.map()
Array.prototype.myMap = function(callback) {
// Your implementation here — no built-in map() allowed
};
// Test:
console.log([1, 2, 3].myMap(x => x * 2)); // [2, 4, 6]
console.log([1, 2, 3].myMap((x, i) => x + i)); // [1, 3, 5]
// 2. Array.prototype.myFilter — behaves exactly like Array.filter()
Array.prototype.myFilter = function(callback) {
// Your implementation
};
// Test:
console.log([1,2,3,4,5].myFilter(x => x % 2 === 0)); // [2, 4]
// 3. Array.prototype.myReduce — behaves exactly like Array.reduce()
Array.prototype.myReduce = function(callback, initialValue) {
// Your implementation — handle the case where initialValue is omitted
};
// Test:
console.log([1,2,3,4,5].myReduce((acc, x) => acc + x, 0)); // 15
console.log([1,2,3,4,5].myReduce((acc, x) => acc + x)); // 15 (no initial value)
Hints:
- Each method takes a
callback(element, index, array)signature mapalways returns an array of the same lengthfilterreturns a new array with only elements where callback returns truthyreducewithout an initial value uses the first element as the accumulator and starts from index 1
Exercise 4 — Babel Mental Transpiler
Objective: Manually “transpile” ES6+ code to ES5, as Babel would do.
Transpile these ES6 snippets to ES5:
// ES6 Input 1 — Arrow function
const greet = name => `Hello, ${name}!`;
// ES5 Output: ???
// ES6 Input 2 — Destructuring with defaults
const { brand = "Unknown", model, year: productionYear } = car;
// ES5 Output: ???
// ES6 Input 3 — Class
class Rectangle {
constructor(width, height) {
this.width = width;
this.height = height;
}
get area() { return this.width * this.height; }
toString() { return `${this.width} × ${this.height}`; }
}
// ES5 Output: ???
// ES6 Input 4 — Rest and spread
function logAll(first, ...rest) {
console.log(first, ...rest);
}
// ES5 Output: ???
Self-check: After writing your ES5 versions, run both the ES6 and ES5 versions in a browser console and verify they produce identical output for the same inputs.
PHASE 3 — PROJECT SIMULATION
Project: JavaScript Time Machine — Era-Aware Code Library
Overview: You will build a single utility library three times — once per major era (ES3, ES5, ES6+). The library handles a product catalogue with search, filtering, and display. By the end, you will have three files that do the same thing in completely different styles, demonstrating how the language evolved.
Real-world connection: Legacy code migration is one of the most common tasks in professional development. Every company with more than five years of JavaScript history has code spanning multiple eras. Being able to read, understand, and modernise old code is one of the most valuable skills a developer can have — and commands premium salaries.
Stage 1 — The ES3 Version (The Original)
// catalogue-es3.js — Written as if it's 2005
// No strict mode, no JSON, no array methods, no AJAX
var CATALOGUE = (function() {
// Private data in IIFE scope
var products = [
{ id: 1, name: "Laptop Pro", price: 450000, category: "electronics", stock: 15 },
{ id: 2, name: "Office Chair", price: 85000, category: "furniture", stock: 8 },
{ id: 3, name: "Wireless Mouse", price: 12000, category: "electronics", stock: 42 },
{ id: 4, name: "Standing Desk", price: 125000, category: "furniture", stock: 5 },
{ id: 5, name: "Notebook A5", price: 1200, category: "stationery", stock: 200}
];
// Private helper — manual string padding (padStart doesn't exist in ES3)
function padLeft(str, length, char) {
str = String(str);
char = char || " ";
while (str.length < length) {
str = char + str;
}
return str;
}
// Private helper — manual array filter
function filterProducts(criteria) {
var result = [];
for (var i = 0; i < products.length; i++) {
var p = products[i];
var matches = true;
if (criteria.category && p.category !== criteria.category) {
matches = false;
}
if (criteria.maxPrice && p.price > criteria.maxPrice) {
matches = false;
}
if (criteria.inStock && p.stock <= 0) {
matches = false;
}
if (matches) {
result.push(p);
}
}
return result;
}
// Private helper — manual sort
function sortByPrice(arr, ascending) {
var sorted = arr.slice(0); // Copy the array
sorted.sort(function(a, b) {
return ascending ? a.price - b.price : b.price - a.price;
});
return sorted;
}
// Public API
return {
search: function(criteria) {
return filterProducts(criteria || {});
},
sortedByPrice: function(ascending) {
return sortByPrice(products, ascending !== false);
},
formatProduct: function(p) {
var priceStr = padLeft("N" + p.price, 12);
var nameStr = p.name;
while (nameStr.length < 20) { nameStr = nameStr + " "; }
return nameStr + priceStr + " [" + p.stock + " in stock]";
},
printAll: function() {
var all = this.search();
for (var i = 0; i < all.length; i++) {
// In a real ES3 app, this might write to the document
// document.write(this.formatProduct(all[i]) + "<br>");
// For our purposes, we use console.log
var s = "";
s += all[i].name;
while (s.length < 20) s += " ";
s += " - N" + all[i].price;
var output = s;
if (typeof console !== "undefined") {
console.log(output);
}
}
}
};
})();
// Usage:
CATALOGUE.printAll();
var electronics = CATALOGUE.search({ category: "electronics", inStock: true });
Stage 2 — The ES5 Refactor
// catalogue-es5.js — Written as if it's 2012
// Uses: strict mode, JSON, array methods, Object.create, bind
"use strict";
var Catalogue = (function() {
var PRODUCTS = Object.freeze([
{ id: 1, name: "Laptop Pro", price: 450000, category: "electronics", stock: 15 },
{ id: 2, name: "Office Chair", price: 85000, category: "furniture", stock: 8 },
{ id: 3, name: "Wireless Mouse", price: 12000, category: "electronics", stock: 42 },
{ id: 4, name: "Standing Desk", price: 125000, category: "furniture", stock: 5 },
{ id: 5, name: "Notebook A5", price: 1200, category: "stationery", stock: 200}
]);
function CatalogueInstance(products) {
this._products = products;
}
CatalogueInstance.prototype = {
constructor: CatalogueInstance,
search: function(criteria) {
criteria = criteria || {};
return this._products.filter(function(p) {
if (criteria.category && p.category !== criteria.category) return false;
if (criteria.maxPrice && p.price > criteria.maxPrice) return false;
if (criteria.inStock && p.stock <= 0) return false;
return true;
});
},
getNames: function() {
return this._products.map(function(p) { return p.name; });
},
getTotalValue: function() {
return this._products.reduce(function(total, p) {
return total + p.price;
}, 0);
},
sortByPrice: function(ascending) {
return this._products.slice(0).sort(function(a, b) {
return ascending !== false ? a.price - b.price : b.price - a.price;
});
},
formatProduct: function(p) {
return JSON.stringify({
name: p.name,
price: "N" + p.price.toLocaleString(),
category: p.category,
inStock: p.stock > 0
}, null, 2);
},
printSummary: function() {
var self = this;
this._products.forEach(function(p) {
var stockStatus = p.stock > 0 ? "(" + p.stock + " in stock)" : "(OUT OF STOCK)";
console.log(p.name + " - N" + p.price.toLocaleString() + " " + stockStatus);
});
console.log("Total inventory value: N" + this.getTotalValue().toLocaleString());
}
};
return new CatalogueInstance(PRODUCTS);
})();
// Usage:
Catalogue.printSummary();
var furniture = Catalogue.search({ category: "furniture" });
console.log("Furniture items:", furniture.map(function(p) { return p.name; }));
Stage 3 — The ES6+ Modern Version
// catalogue-es6.js — Written in 2024
// Full modern JavaScript
const PRODUCTS = Object.freeze([
{ id: 1, name: "Laptop Pro", price: 450000, category: "electronics", stock: 15 },
{ id: 2, name: "Office Chair", price: 85000, category: "furniture", stock: 8 },
{ id: 3, name: "Wireless Mouse", price: 12000, category: "electronics", stock: 42 },
{ id: 4, name: "Standing Desk", price: 125000, category: "furniture", stock: 5 },
{ id: 5, name: "Notebook A5", price: 1200, category: "stationery", stock: 200}
]);
class Catalogue {
#products;
constructor(products = PRODUCTS) {
this.#products = [...products];
}
// ES2024: Object.groupBy
get byCategory() {
return Object.groupBy(this.#products, p => p.category);
}
// ES2023: toSorted (non-mutating)
sortedByPrice(ascending = true) {
return this.#products.toSorted((a, b) =>
ascending ? a.price - b.price : b.price - a.price
);
}
search({ category = null, maxPrice = Infinity, inStock = false } = {}) {
return this.#products.filter(p =>
(!category || p.category === category) &&
(p.price <= maxPrice) &&
(!inStock || p.stock > 0)
);
}
get totalValue() {
return this.#products.reduce((sum, p) => sum + p.price, 0);
}
// ES2022: at() for last element access
get mostExpensive() { return this.sortedByPrice(false).at(0); }
get leastExpensive() { return this.sortedByPrice(true).at(0); }
formatProduct({ name, price, category, stock }) {
const status = stock > 0 ? `${stock} in stock` : "OUT OF STOCK";
return `${name.padEnd(20)} ₦${price.toLocaleString().padStart(10)} [${status}]`;
}
printSummary() {
console.log("=".repeat(55));
console.log("PRODUCT CATALOGUE");
console.log("=".repeat(55));
this.#products.forEach(p => console.log(this.formatProduct(p)));
console.log("─".repeat(55));
console.log(`Total inventory value: ₦${this.totalValue.toLocaleString()}`);
console.log(`Most expensive: ${this.mostExpensive.name}`);
console.log(`Least expensive: ${this.leastExpensive.name}`);
}
// ES2025: Set methods for tag/category operations
compareCategoryAvailability(cat1, cat2) {
const names1 = new Set(this.search({ category: cat1 }).map(p => p.name));
const names2 = new Set(this.search({ category: cat2 }).map(p => p.name));
return {
onlyInFirst: [...names1.difference(names2)],
onlyInSecond: [...names2.difference(names1)],
inBoth: [...names1.intersection(names2)]
};
}
toJSON() {
return {
count: this.#products.length,
totalValue: this.totalValue,
categories: Object.keys(this.byCategory),
products: this.#products
};
}
}
const catalogue = new Catalogue();
catalogue.printSummary();
const electronics = catalogue.search({ category: "electronics", inStock: true });
console.log("\nIn-stock electronics:");
electronics.forEach(p => console.log(` ${p.name}`));
console.log("\nJSON export:");
console.log(JSON.stringify(catalogue.toJSON(), null, 2));
Reflection Questions
-
Looking at the three versions side-by-side, which specific ES3 limitation caused the most code complexity? Would that problem have been solved by ES5 alone, or did it require ES6?
-
The IIFE pattern in the ES3 and ES5 versions was the standard way to create modules for many years. ES6 modules replaced it. What are the advantages of
import/exportover the IIFE pattern for large codebases? -
The ES5 version uses
"use strict"but the ES3 version does not. What would break in the ES3 version if you added"use strict"to it? -
Why did Microsoft’s dominance with Internet Explorer actually slow down JavaScript’s evolution? What economic and technical factors caused the ten-year gap between ES3 (1999) and ES5 (2009)?
-
Babel transforms ES6 classes back into ES5 prototype code. If the output is functionally identical, why do developers still prefer writing ES6 class syntax? What does this tell us about the purpose of syntactic sugar?
-
The
argumentsobject in ES3/ES5 and the...restparameter in ES6 both collect extra function arguments. Write a concrete example whereargumentswould fail and...restwould succeed — and explain why.
Advanced Challenges (Optional)
-
Build a Babel-Lite: Write a JavaScript function that takes a simple arrow function string like
"x => x * 2"and returns the ES5 equivalent"function(x) { return x * 2; }". Use regex and string manipulation — noeval. -
Feature Detection Library: Build a
supportsobject that detects whether the current environment supports ES5, ES6, and ES2020 features. Use feature detection (not user agent sniffing). Example:supports.es6.classes,supports.es2020.optionalChaining. -
Version Timeline Visualiser: Build an HTML page that displays a visual timeline of ECMAScript versions, showing which features each version introduced. Clicking a feature should show a code example. Use only the JavaScript features available in that version to implement the UI (i.e., the ES3 timeline must be built with ES3 code).
-
Cross-Version Compatibility Checker: Write a function that takes JavaScript code as a string and identifies the minimum ES version required to run it (by detecting which features are used). For example, if the code contains
?., report “Requires ES2020+”. -
Node.js Version Matrix: Research how Node.js version numbers map to V8 engine versions and ES support. Build a table showing: Node 12, 14, 16, 18, 20, 22 and which ES features each supports natively without Babel.
Completion Checklist
- Version timeline: Know all versions from ES1 (1997) through ES2026 — why ES4 was cancelled
- Three eras: Wild West (1995–2009), Stabilisation (2009–2015), Modern (2015–present)
- JavaScript origin story: Brendan Eich, 10 days, Mocha → LiveScript → JavaScript
- Browser wars: Netscape vs Microsoft JScript, ECMA standardisation
- ES4 failure: Why it was abandoned, how ideas resurfaced in ES6
- Node.js (2009): Why server-side JS was transformative
- ES3 (1999):
try/catch,switch, regex,do...while,split/replace - ES3 limitations: No
let/const, no JSON, no array methods, no strict mode - IIFE pattern: Why it existed, how it simulated block scope and modules
argumentsobject: How it works; why rest parameters replaced it- ES5 (2009) —
"use strict": What it catches; list of prevented behaviours - ES5 —
JSON.parse()/JSON.stringify(): Complete options — reviver, replacer, space - ES5 — Array methods:
forEach,map,filter,reduce,indexOf,every,some,isArray - ES5 —
Object.create(): Prototype-based inheritance withoutnew - ES5 — Property descriptors:
defineProperty,freeze,seal,keys - ES5 —
Function.bind(): Solving thethisproblem - ES6 —
let/const: Block scope, TDZ, const with objects - ES6 — Arrow functions: Concise syntax; lexical
thisbinding - ES6 — Template literals: Interpolation, multi-line, tagged templates
- ES6 — Destructuring: Array, object, nested, defaults, rename, parameter
- ES6 — Default parameters: Prevent undefined bugs; can reference earlier params
- ES6 — Rest/Spread: Collecting and expanding — arrays and objects
- ES6 — Classes: Syntax sugar over prototypes;
extends/super; static methods - ES6 — Promises: Three states;
.then().catch().finally(); chaining - ES6 — Modules:
import/export; default vs named;* asnamespace - ES6 —
Map/Set: Use cases vs plain objects/arrays; iteration - ES6 — Symbols: Guaranteed uniqueness; well-known symbols; hidden keys
- ES6 — Generators:
function*,yield, creating iterables - IE history: IE6 through IE11; the compatibility nightmare; JScript divergence
- Edge: Legacy EdgeHTML vs Chromium Edge; IE retirement (June 2022)
- Feature detection vs browser detection: Why feature detection is correct
- Polyfills: What they are;
Array.from,String.includesexamples - Babel transpilation: What it does; reading transpiled output
- Can I Use / MDN: How to check browser support for any feature
- Exercises completed: ES3/ES5/ES6 rewrite, version detective, polyfill writing, mental transpiler
- Full project completed: Three-era catalogue library with complete side-by-side comparison
- Reflection questions answered
One-sentence summary: JavaScript was born in ten days in 1995, spent a decade locked at ES3 while browser wars raged and ES4 was abandoned, found stability in ES5’s strict mode and native JSON, then transformed into a modern professional language with ES6’s classes, modules, Promises and arrow functions — a thirty-year journey from a Netscape scripting toy to the most widely deployed programming language on Earth.