What is ES6+?
In June 2015, JavaScript received its biggest update ever. ECMAScript 2015, better known as ES6, added block scoping, arrow functions, classes, modules, promises and much more. The committee that maintains the language then committed to a new edition every year, and “ES6+” has become shorthand for that entire modern era.
You do not need to learn every feature. A handful of them account for the vast majority of everyday code, and they are what this guide covers. The theme running through all of them is simple: say what you mean with less ceremony.
let and const
The oldest feature is also the most important. var is function-scoped and hoisted in ways that surprise people. let and const are block-scoped, so they exist only inside the nearest { }.
// scope.js
if (true) {
let message = "inside";
const limit = 10;
}
// message and limit are not defined out here
const prevents reassignment of the binding, which makes your intent obvious: this name will not point somewhere else. It does not freeze the value, so a const array can still be mutated. Use const by default, let when reassignment is real, and treat var as history.
Arrow functions
Arrow functions are a shorter syntax with one important behavioural difference: they do not have their own this. They inherit this from the surrounding scope, which is almost always what you want in callbacks.
// arrows.js
const add = (a, b) => a + b;
const square = (n) => {
return n * n;
};
const names = ["Ada", "Grace", "Linus"];
const upper = names.map((name) => name.toUpperCase());
When the body is a single expression, the braces and return can be omitted. Parentheses around a single parameter are optional. Because arrow functions capture this, they are ideal for event handlers and array methods, but a poor fit for object methods that need their own this.
Template literals
Backticks create strings that span multiple lines and interpolate expressions with ${ }.
// message.js
const name = "Ada";
const role = "engineer";
const greeting = `Hello, ${name}!
You are logged in as a ${role}.`;
Template literals also support tagged templates, where a function receives the literal parts and the interpolated values separately. That mechanism underpins many templating and styling libraries.
Destructuring
Destructuring unpacks values from arrays and objects into variables, which removes a lot of repetitive property access.
// destructure.js
const user = { name: "Ada", age: 36, city: "London" };
const { name, age, city = "Unknown" } = user;
const coords = [51.5, -0.12];
const [lat, lng] = coords;
function greet({ name }) {
return `Hello, ${name}`;
}
You can rename while destructuring (const { name: userName } = user), provide defaults, and destructure directly in a function’s parameter list — a pattern used constantly with React props and configuration objects.
Spread and rest
The same three dots do two complementary jobs depending on where they appear.
// spread.js
const a = [1, 2, 3];
const b = [4, 5];
const combined = [...a, ...b]; // spread: expand
const copy = { ...user, age: 37 }; // shallow clone + override
function sum(...numbers) { // rest: collect
return numbers.reduce((t, n) => t + n, 0);
}
Spread copies into a new array or object, which is the backbone of immutable updates. Rest gathers remaining arguments or elements into a single array. Together they make working with collections far more pleasant.
Default parameters and shorthand
Functions can declare default values, and object literals can use shorthand when the property name matches a variable.
// config.js
function connect({ host = "localhost", port = 8080 } = {}) {
return `${host}:${port}`;
}
const name = "Ada";
const user = { name, greet() { return `Hi, ${this.name}`; } };
Defaults are evaluated only when the argument is undefined, which is usually what you want. Shorthand properties and methods cut noise from object-heavy code.
Optional chaining and nullish coalescing
These two operators, added in 2020, eliminated mountains of defensive checks.
// safe.js
const city = user?.address?.city;
const name = input ?? "anonymous";
user?.profile?.onLogin?.();
?. short-circuits to undefined if the value on its left is null or undefined. ?? provides a fallback only when the left side is null or undefined — unlike ||, it does not treat 0, "" or false as missing.
Array methods
The array prototype is packed with methods that describe intent instead of looping mechanics.
// arrays.js
const products = [
{ name: "Keyboard", price: 80, inStock: true },
{ name: "Mouse", price: 40, inStock: false },
{ name: "Cable", price: 10, inStock: true },
];
const names = products.map((p) => p.name);
const cheap = products.filter((p) => p.price < 50);
const total = products.reduce((sum, p) => sum + p.price, 0);
const available = products.some((p) => p.inStock);
const allCheap = products.every((p) => p.price < 100);
const mouse = products.find((p) => p.name === "Mouse");
map transforms, filter selects, reduce folds a list into one value, find returns the first match, and some/every test a condition. They chain, they do not mutate the original array, and they read like a description of what you want.
Modules
ES modules let you split code across files and share only what you choose.
// math.js
export function add(a, b) {
return a + b;
}
export const PI = 3.14159;
export default function subtract(a, b) {
return a - b;
}
// app.js
import subtract, { add, PI } from "./math.js";
Named exports can be several per file and are imported by name. A default export is a single primary value. Modules have their own scope, are evaluated once, and let bundlers remove unused code through tree-shaking.
Classes
Classes provide a clear syntax for object-oriented code, built on JavaScript’s existing prototype system.
// user.js
class User {
#secret = "hidden"; // private field
constructor(name) {
this.name = name;
}
get greeting() {
return `Hello, ${this.name}`;
}
static create(name) {
return new User(name);
}
}
class Admin extends User {
constructor(name) {
super(name);
this.role = "admin";
}
}
extends and super handle inheritance, static defines methods on the class itself, and # marks truly private fields. Under the hood it is still prototypes, but the syntax is far easier to read.
Other features worth knowing
- Map and Set for keyed collections and unique values, with better semantics than plain objects and arrays for those jobs.
- for…of to iterate arrays, strings, maps, sets and any iterable.
- Exponentiation with
**, so2 ** 10replacesMath.pow(2, 10). - Iterators and generators for lazy sequences, custom iteration and pausable functions.
- Object.entries / fromEntries to convert between objects and arrays of pairs.
Best practices
- Default to
const; useletonly when you reassign. - Prefer arrow functions for callbacks, regular functions for methods that need
this. - Destructure in parameters to make required shape explicit.
- Use spread for immutable updates instead of mutating shared data.
- Reach for
?.and??before writing nested guards. - Prefer array methods over hand-written loops when transforming data.
- Keep modules small and export the minimum surface.
Common mistakes
- Assuming
constmakes an object immutable — it only prevents rebinding. - Using an arrow function as an object method and losing
this. - Using
||for defaults when0or""are valid values; use??. - Spreading only one level deep and expecting a deep clone.
- Mixing
requireandimportin the same module system without understanding the interop. - Chaining too many array methods into an unreadable pipeline.
Where to go next
Modern syntax is the vocabulary; the DOM and asynchronous JavaScript are where you put it to work. Once promises and modules feel natural, the Fetch API turns those skills into real applications.