A practical, easy-to-read guide to the major features introduced with ES6 (ECMAScript 2015), with concise explanations and JavaScript examples.
Welcome to the comprehensive guide to ECMAScript 6 (ES6), also known as ECMAScript 2015. ES6 is a monumental update to the JavaScript language, marking the first major update since ES5 was standardized in 2009. This update introduced a massive amount of syntax sugar, new APIs, and paradigms that make JavaScript more powerful, readable, and maintainable.
Below is an exhaustive, well-organized, and easy-to-understand breakdown of every feature introduced in ES6, complete with awesome, practical examples.
- Arrow Functions
- Classes
- Enhanced Object Literals
- Template Strings
- Destructuring Assignment
- Default Parameters
- Rest & Spread Operators
- Let and Const (Block Scoping)
- Iterators and
for...ofLoops - Generators
- Unicode
- Modules
- Module Loaders
- Map, Set, WeakMap, WeakSet
- Proxy
- Symbols
- Subclassable Built-ins
- Promises
- New APIs (Math, Number, String, Array, Object)
- Binary and Octal Literals
- Reflect API
- Tail Call Optimization
Arrow functions provide a shorter, more concise syntax for writing function expressions. Crucially, they do not bind their own this (they inherit this from the enclosing execution context), which solves a major pain point in pre-ES6 JavaScript.
Example:
// ES5
var multiply = function(a, b) {
return a * b;
};
// ES6 - Concise body (implicit return)
const multiplyES6 = (a, b) => a * b;
// ES6 - Block body (explicit return needed)
const multiplyAndLog = (a, b) => {
const result = a * b;
console.log(result);
return result;
};
// Lexical 'this' example
function Timer() {
this.seconds = 0;
// Arrow function inherits 'this' from Timer()
setInterval(() => {
this.seconds++; // 'this' refers to the Timer instance!
}, 1000);
}ES6 introduces a class syntax that is syntactic sugar over JavaScript's existing prototype-based inheritance. It provides a much cleaner, object-oriented syntax for creating objects and dealing with inheritance.
Example:
class Animal {
constructor(name) {
this.name = name;
}
speak() {
console.log(`${this.name} makes a sound.`);
}
}
class Dog extends Animal {
constructor(name) {
super(name); // Call the parent class constructor
}
speak() {
super.speak(); // Call parent method
console.log(`${this.name} barks!`);
}
}
const dog = new Dog("Rex");
dog.speak();
// Output: Rex makes a sound. \n Rex barks!Object literals are easier to write. You can now use shorthand for properties and methods, compute property names at runtime, and use __proto__ for prototype manipulation.
Example:
const name = "John";
const age = 30;
// Shorthand properties and methods
const person = {
name, // Equivalent to name: name
age, // Equivalent to age: age
greet() { // Equivalent to greet: function()
console.log(`Hello, I'm ${this.name}`);
},
// Computed property name
[`dynamicKey_${age}`]: true
};
console.log(person.dynamicKey_30); // true
person.greet();Template literals use backticks (`) instead of quotes. They allow for multi-line strings and string interpolation (embedding variables/expressions directly into the string).
Example:
const user = { name: "Alice", age: 25 };
// String interpolation
const greeting = `Hello, my name is ${user.name} and I am ${user.age + 5} years old.`;
console.log(greeting); // "Hello, my name is Alice and I am 30 years old."
// Multi-line strings
const html = `
<div>
<h1>${user.name}</h1>
</div>
`;Destructuring allows you to extract data from arrays or objects into distinct variables using a simplified, pattern-matching syntax.
Example:
// Object Destructuring
const user = { name: "Bob", age: 40, role: "Admin" };
const { name, role } = user;
console.log(name, role); // "Bob" "Admin"
// Array Destructuring
const numbers = [10, 20, 30];
const [first, second] = numbers;
console.log(first, second); // 10 20
// Swapping variables without a temporary variable
let a = 1, b = 2;
[a, b] = [b, a];
console.log(a, b); // 2 1
// Default values in destructuring
const { city = "Unknown" } = user;
console.log(city); // "Unknown"You can now assign default values to function parameters directly in the function signature, eliminating the need for || fallback hacks.
Example:
// ES6
function calculatePrice(price, taxRate = 0.07, shipping = 5) {
return price + (price * taxRate) + shipping;
}
console.log(calculatePrice(100)); // Uses defaults: 100 + 7 + 5 = 112
console.log(calculatePrice(100, 0.05)); // Overrides taxRate: 100 + 5 + 5 = 110The ... operator serves two distinct purposes depending on where it is used:
- Rest: Gathers multiple elements into a single array (usually in function parameters).
- Spread: Expands an iterable (like an array) into individual elements.
Example:
// REST: Gather arguments into an array
function sum(...numbers) {
return numbers.reduce((acc, curr) => acc + curr, 0);
}
console.log(sum(1, 2, 3, 4)); // 10
// SPREAD: Expand arrays/objects
const arr1 = [1, 2, 3];
const arr2 = [...arr1, 4, 5]; // [1, 2, 3, 4, 5]
const obj1 = { a: 1, b: 2 };
const obj2 = { ...obj1, c: 3 }; // { a: 1, b: 2, c: 3 } (Object spread added in ES2018, but conceptually linked)Before ES6, JavaScript only had function-level scope via var. ES6 introduces block-level scoping.
let: Variables that can be reassigned but are scoped to their nearest enclosing block.const: Variables that cannot be reassigned after declaration (though objects/arrays assigned toconstcan still be mutated).
Example:
function varVsLet() {
if (true) {
var varVar = "I exist everywhere in this function";
let letVar = "I only exist inside this 'if' block";
const constVar = "I am constant and scoped to this block";
// constVar = "New value"; // TypeError: Assignment to constant variable.
}
console.log(varVar); // "I exist everywhere..."
// console.log(letVar); // ReferenceError: letVar is not defined
}
varVsLet();ES6 introduces a new iteration protocol. The for...of loop iterates over the values of an iterable object (like Arrays, Strings, Maps, Sets), unlike for...in which iterates over keys (indices).
Example:
const fruits = ["Apple", "Banana", "Cherry"];
// for...in loops over indices (keys)
for (const index in fruits) {
console.log(index); // "0", "1", "2"
}
// for...of loops over values
for (const fruit of fruits) {
console.log(fruit); // "Apple", "Banana", "Cherry"
}
// Works on strings too!
for (const char of "Hi") {
console.log(char); // "H", "i"
}Generators are special functions that can be paused and resumed. Denoted by function*, they use the yield keyword to return a value and pause execution until .next() is called again.
Example:
function* idGenerator() {
let id = 1;
while (true) {
yield id++; // Pause here and return the id
}
}
const generator = idGenerator();
console.log(generator.next().value); // 1
console.log(generator.next().value); // 2
console.log(generator.next().value); // 3ES6 adds better support for Unicode. It introduces a new u flag for regular expressions and a new escape syntax for code points: \u{...}, solving issues with surrogate pairs (like emojis).
Example:
// Old way: Only handles up to \uFFFF
console.log("\u0041"); // "A"
// ES6 way: Handles all Unicode code points
console.log("\u{1F680}"); // "🚀"
// Regex with 'u' flag
const regex = /^\u{1F680}$/u;
console.log(regex.test("🚀")); // trueNative module support finally arrived in ES6. You can split your code into separate files and explicitly export and import functionality.
Example:
// --- math.js ---
export const add = (a, b) => a + b;
export default function multiply(a, b) { return a * b; }
// --- main.js ---
import multiply, { add } from './math.js';
// Default imports don't need curly braces, named imports do.
console.log(add(2, 3)); // 5
console.log(multiply(2, 3)); // 6ES6 specifies a programmatic API for working with modules dynamically. While bundlers (like Webpack/Rollup) handle this statically at build time, the native dynamic import() allows for lazy-loading modules on the fly. (Note: import() was formalized in ES2020 but is conceptually part of the ES6 module system dynamic loader).
Example:
// Load a module dynamically based on a user action
document.getElementById('btn').addEventListener('click', async () => {
// Dynamically import the module
const module = await import('./heavyModule.js');
module.doSomethingHeavy();
});New data structures for handling collections.
- Set: A collection of unique values.
- Map: A collection of key-value pairs where keys can be of any type (unlike Objects).
- WeakMap/WeakSet: Keys must be objects, and they allow for garbage collection of the keys if there are no other references to them.
Example:
// Set: Removes duplicates
const uniqueTags = new Set(["JS", "JS", "CSS"]);
console.log(uniqueTags.size); // 2
// Map: Non-string keys
const userMap = new Map();
const userObj = { name: "Sarah" };
userMap.set(userObj, "Active");
console.log(userMap.get(userObj)); // "Active"
// WeakMap: Keys are weakly held (memory safe)
const weakMap = new WeakMap();
let tempUser = {};
weakMap.set(tempUser, "data");
tempUser = null; // The entry in weakMap is now eligible for garbage collectionProxies allow you to create an object that intercepts and redefines fundamental operations for another object, like property lookup, assignment, and function invocation.
Example:
const target = {
message: "Hello"
};
const handler = {
get: function(obj, prop) {
return prop in obj ? obj[prop] : "Property does not exist!";
}
};
const proxy = new Proxy(target, handler);
console.log(proxy.message); // "Hello"
console.log(proxy.nonExistent); // "Property does not exist!"Symbols are a primitive data type (like strings or numbers) that are guaranteed to be unique and immutable. They are primarily used to add hidden, non-colliding properties to objects.
Example:
const classId = Symbol('classId');
const classId2 = Symbol('classId');
console.log(classId === classId2); // false! Symbols are unique.
const classroom = {
students: 30,
[classId]: "Math-101" // Hidden property
};
console.log(Object.keys(classroom)); // ['students'] (Symbols don't show up)
console.log(classroom[classId]); // "Math-101"In ES5, you couldn't easily extend built-in types like Array, Error, or Date. ES6 fixes this by making built-ins properly "subclassable" using the extends keyword.
Example:
// Creating a custom Array with a built-in "empty" checker
class MyAwesomeArray extends Array {
isEmpty() {
return this.length === 0;
}
}
const myArr = new MyAwesomeArray(1, 2, 3);
console.log(myArr.isEmpty()); // false
console.log(myArr instanceof Array); // true
// Custom Error class
class ValidationError extends Error {
constructor(message) {
super(message);
this.name = "ValidationError";
}
}Promises provide a cleaner, more robust way to handle asynchronous operations compared to deeply nested callbacks (callback hell). A Promise represents the eventual completion (or failure) of an operation.
Example:
const fetchUserData = (success) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (success) resolve("User data retrieved!");
else reject(new Error("Failed to fetch user data."));
}, 1000);
});
};
fetchUserData(true)
.then(data => console.log(data))
.catch(err => console.error(err.message));
// Output after 1 second: "User data retrieved!"ES6 added a massive amount of functionality to standard libraries like Math, Number, String, Array, and Object.
Examples:
// --- Number ---
console.log(Number.isInteger(42)); // true
console.log(Number.isNaN("NaN")); // false (stricter than global isNaN)
// --- Math ---
console.log(Math.sign(-5)); // -1
console.log(Math.trunc(4.9)); // 4 (removes decimals)
// --- String ---
const str = "Hello World";
console.log(str.startsWith("Hello")); // true
console.log(str.endsWith("World")); // true
console.log(str.includes("o W")); // true
console.log("abc".repeat(3)); // "abcabcabc"
// --- Array ---
const arr = [1, 2, 3, 4];
console.log(Array.from("hello")); // ['h','e','l','l','o'] (array-like to array)
console.log(Array.of(1, 2, 3)); // [1, 2, 3]
console.log(arr.find(x => x > 2)); // 3 (first matching element)
console.log(arr.findIndex(x => x > 2));// 2 (index of first match)
// --- Object ---
const obj = { a: 1, b: 2 };
console.log(Object.keys(obj)); // ['a', 'b']
console.log(Object.values(obj)); // [1, 2] (Formalized in ES2017, conceptually ES6)
console.log(Object.assign({}, obj, { c: 3 })); // { a: 1, b: 2, c: 3 }ES6 allows you to write numbers in binary (0b) and octal (0o) formats directly, improving readability when dealing with bitwise operations or file permissions.
Example:
// Binary (base 2)
const binary = 0b1010;
console.log(binary); // 10
// Octal (base 8)
const octal = 0o755;
console.log(octal); // 493 (Standard Unix file permission)The Reflect object provides interceptable methods for JavaScript operations. It houses the internal methods that the Proxy traps call, making meta-programming cleaner and standardizing some older quirks.
Example:
const user = { name: "Eve", age: 25 };
// Reflect.get instead of user['name']
console.log(Reflect.get(user, "name")); // "Eve"
// Reflect.has instead of 'name' in user
console.log(Reflect.has(user, "age")); // true
// Reflect.ownKeys returns all keys (including Symbols)
console.log(Reflect.ownKeys(user)); // ['name', 'age']Tail Call Optimization (TCO) is an ES6 feature where, if a function returns the result of calling another function (a "tail call"), the JavaScript engine can reuse the current stack frame instead of creating a new one. This prevents "Maximum call stack size exceeded" errors in recursive functions.
(Note: While in the ES6 spec, engine support for TCO varies, most notably Safari/JSC supports it, while V8/Chrome currently does not).
Example:
// Strict mode is required for TCO
'use strict';
// Calculating factorial using tail recursion
function factorial(n, acc = 1) {
if (n <= 1) return acc;
// The LAST thing the function does is return the result of calling itself.
// Because it does no further math on the return value, this is a "Tail Call".
return factorial(n - 1, n * acc);
}
console.log(factorial(5)); // 120Document successfully covers all features introduced in ECMAScript 6 (ES2015).