ES6 (ECMAScript 2015) was the biggest update to JavaScript in the language's history. Combined with the improvements that followed in ES2017 through ES2024, modern JavaScript is a much more expressive and powerful language than it was a decade ago. This guide covers the features you'll use every day, with practical examples and the "why" behind each one.
Arrow Functions
Arrow functions provide a concise syntax and, importantly, don't have their own this binding — they inherit this from the surrounding scope.
// Traditional function
const add = function(a, b) { return a + b; };
// Arrow function — same thing, shorter
const add = (a, b) => a + b;
// Single parameter — parens optional
const double = n => n * 2;
// No parameters — parens required
const getTimestamp = () => Date.now();
// Multi-line body — needs curly braces and explicit return
const processUser = (user) => {
const fullName = `${user.first} ${user.last}`;
return { ...user, fullName };
};
// Returning an object literal — wrap in parens
const makePoint = (x, y) => ({ x, y });
// The this binding difference
class Timer {
constructor() {
this.count = 0;
}
start() {
// Arrow function: 'this' is the Timer instance
setInterval(() => {
this.count++;
console.log(this.count);
}, 1000);
// Regular function would break: 'this' would be undefined (strict mode)
}
}
Destructuring
Destructuring lets you unpack values from arrays and objects into distinct variables.
Array Destructuring
const [first, second, third] = [10, 20, 30];
console.log(first); // 10
// Skip elements with commas
const [,, third] = [1, 2, 3];
console.log(third); // 3
// Default values
const [a = 0, b = 0, c = 0] = [1, 2];
console.log(c); // 0
// Swap variables without a temp
let x = 1, y = 2;
[x, y] = [y, x];
console.log(x, y); // 2 1
// Rest element
const [head, ...tail] = [1, 2, 3, 4, 5];
console.log(head); // 1
console.log(tail); // [2, 3, 4, 5]
Object Destructuring
const user = { id: 1, name: 'Alice', age: 30, role: 'admin' };
const { name, age } = user;
console.log(name); // 'Alice'
// Rename while destructuring
const { name: fullName, role: userRole } = user;
console.log(fullName); // 'Alice'
// Default values
const { name, city = 'Unknown' } = user;
console.log(city); // 'Unknown'
// Nested destructuring
const { address: { street, zip } = {} } = user;
// Rest properties
const { id, ...rest } = user;
console.log(rest); // { name: 'Alice', age: 30, role: 'admin' }
// In function parameters — very common pattern
function displayUser({ name, role, age = 0 }) {
console.log(`${name} (${role}), age ${age}`);
}
displayUser(user);
// Destructuring API responses
async function loadUser(id) {
const { data: { user: { name, email } } } = await fetchUser(id);
return { name, email };
}
Template Literals
const name = 'World';
const greeting = `Hello, ${name}!`;
// Multi-line strings
const html = `
## ${user.name}
${user.bio}
`;
// Any expression works inside ${}
const price = 9.99;
const tax = 0.08;
const total = `Total: $${(price * (1 + tax)).toFixed(2)}`;
// Tagged templates (advanced)
function highlight(strings, ...values) {
return strings.reduce((result, str, i) => {
const val = values[i] !== undefined ? `<mark>${values[i]}</mark>` : '';
return result + str + val;
}, '');
}
const name = 'Alice';
const msg = highlight`Welcome back, ${name}! You have ${5} messages.`;
// 'Welcome back, <mark>Alice</mark>! You have <mark>5</mark> messages.'
Spread and Rest Operators
// Rest: collect multiple arguments into an array
function sum(...numbers) {
return numbers.reduce((total, n) => total + n, 0);
}
sum(1, 2, 3, 4, 5); // 15
// Spread: expand an array/object into individual values
// Combine arrays
const a = [1, 2, 3];
const b = [4, 5, 6];
const combined = [...a, ...b]; // [1, 2, 3, 4, 5, 6]
// Copy an array
const copy = [...a];
// Spread into function arguments
Math.max(...numbers);
// Merge objects
const defaults = { theme: 'light', lang: 'en', debug: false };
const userPrefs = { theme: 'dark', lang: 'fr' };
const config = { ...defaults, ...userPrefs };
// { theme: 'dark', lang: 'fr', debug: false }
// Add or override a property immutably
const updated = { ...user, name: 'New Name', updatedAt: new Date() };
// Clone with spread (shallow)
const clone = { ...original };
let and const
// var: function-scoped, hoisted (avoid in modern code)
var x = 1;
// let: block-scoped, reassignable
let count = 0;
count++;
// const: block-scoped, cannot be reassigned (but objects/arrays can be mutated)
const PI = 3.14159;
const user = { name: 'Alice' };
user.name = 'Bob'; // ✅ OK — mutating the object
user = { name: 'Carol' } // ❌ TypeError — reassigning the binding
// Use const by default, let when you need to reassign, never var
Modules (import/export)
// math.js — named exports
export const PI = 3.14159;
export function add(a, b) { return a + b; }
export function multiply(a, b) { return a * b; }
// Rename on export
export { add as sum };
// Default export (one per file)
export default function divide(a, b) { return a / b; }
// app.js — importing
import divide from './math.js'; // default import
import { add, PI } from './math.js'; // named imports
import { add as plus } from './math.js'; // rename on import
import * as math from './math.js'; // import everything
// Re-export (barrel files)
// index.js
export { add, multiply } from './math.js';
export { default as divide } from './math.js';
Classes
class Animal {
#name; // private field (ES2022)
constructor(name, sound) {
this.#name = name;
this.sound = sound;
}
get name() { return this.#name; }
speak() {
return `${this.#name} says ${this.sound}`;
}
static create(name, sound) {
return new Animal(name, sound);
}
}
class Dog extends Animal {
constructor(name) {
super(name, 'woof');
this.tricks = [];
}
learn(trick) {
this.tricks.push(trick);
return this; // enable chaining
}
speak() {
const base = super.speak();
return `${base}! It knows: ${this.tricks.join(', ')}`;
}
}
const dog = new Dog('Rex');
dog.learn('sit').learn('shake');
console.log(dog.speak());
// 'Rex says woof! It knows: sit, shake'
Optional Chaining and Nullish Coalescing
// Optional chaining (?.) — ES2020
const user = null;
// Without optional chaining — throws TypeError
// const city = user.address.city;
// With optional chaining — returns undefined safely
const city = user?.address?.city;
// Works with methods
const result = obj?.method?.();
// Works with arrays/bracket notation
const firstTag = post?.tags?.[0];
// Nullish coalescing (??) — ES2020
// Returns right side only when left is null or undefined (not 0 or '')
const name = user?.name ?? 'Anonymous';
const port = config.port ?? 3000;
// vs logical OR (||) which also triggers on 0, '', false
const port1 = config.port ?? 3000; // 0 stays 0
const port2 = config.port || 3000; // 0 becomes 3000 — bug!
// Nullish coalescing assignment (??=) — ES2021
user.preferences ??= {}; // assign only if null/undefined
user.score ||= 0; // assign if falsy (||=)
user.count &&= user.count + 1; // assign only if truthy (&&=)
Promises and Async/Await
// Covered in detail in the Promises guide
// Key syntax reminder:
async function fetchData(url) {
try {
const response = await fetch(url);
return await response.json();
} catch (error) {
console.error('Failed:', error);
throw error;
}
}
Map and Set
// Set: collection of unique values
const set = new Set([1, 2, 3, 2, 1]);
console.log(set.size); // 3
set.add(4);
set.has(2); // true
set.delete(2);
[...set]; // [1, 3, 4]
// Remove duplicates from an array
const unique = [...new Set([1, 1, 2, 3, 3])]; // [1, 2, 3]
// Map: key-value pairs with any key type
const map = new Map();
const objKey = { id: 1 };
map.set(objKey, 'some value');
map.set('name', 'Alice');
map.get(objKey); // 'some value'
map.size; // 2
map.has('name'); // true
// Iterate
for (const [key, value] of map) {
console.log(key, value);
}
// Convert object to Map
const obj = { a: 1, b: 2, c: 3 };
const mapFromObj = new Map(Object.entries(obj));
// Convert Map back to object
const objFromMap = Object.fromEntries(map);
Symbol, WeakMap, WeakSet, WeakRef
// Symbol: unique, immutable primitive
const sym = Symbol('description');
const sym2 = Symbol('description');
sym === sym2; // false — always unique
// Use as object keys to avoid collisions
const ID = Symbol('id');
const obj = { [ID]: 123, name: 'Alice' };
obj[ID]; // 123
// Well-known symbols
class MyArray {
[Symbol.iterator]() { /* custom iteration */ }
}
// WeakMap: keys must be objects; entries can be garbage collected
const cache = new WeakMap();
function getResult(obj) {
if (cache.has(obj)) return cache.get(obj);
const result = expensiveCalc(obj);
cache.set(obj, result);
return result;
}
Logical Assignment and Other ES2021+ Features
// Logical assignment (ES2021)
a ||= b; // a = a || b
a &&= b; // a = a && b
a ??= b; // a = a ?? b
// Numeric separators (ES2021) — improve readability
const million = 1_000_000;
const hex = 0xFF_EC_D0;
const bytes = 0b1010_0001;
// Object.hasOwn() (ES2022) — safer than hasOwnProperty
Object.hasOwn(obj, 'key'); // true if own property
// Array.at() (ES2022) — supports negative indices
const arr = [1, 2, 3, 4, 5];
arr.at(-1); // 5 (last element)
arr.at(-2); // 4
// String.at() works the same way
'hello'.at(-1); // 'o'
// structuredClone() (ES2022) — deep clone
const original = { a: 1, b: { c: 2 } };
const deep = structuredClone(original);
deep.b.c = 99;
console.log(original.b.c); // 2 — not affected
// Error cause (ES2022)
try {
await connectToDatabase();
} catch (err) {
throw new Error('Failed to load user', { cause: err });
}
Quick Reference
// Variables
const x = 1; // block-scoped, no reassignment
let y = 2; // block-scoped, reassignable
// Functions
const fn = (a, b) => a + b; // arrow function
const fn = async () => {}; // async arrow
function fn(...args) {} // rest params
// Strings
`Hello ${name}` // template literal
// Destructuring
const { a, b } = obj; // object
const [x, y] = arr; // array
function fn({ a, b = 0 } = {}) {} // default params
// Spread/Rest
{ ...obj, key: val } // merge/add
[...arr1, ...arr2] // combine arrays
fn(...arr) // spread as args
// Modules
import def from './mod' // default
import { a, b } from './mod' // named
export default fn // default export
export { a, b } // named exports
// Safe access
obj?.prop?.method?.() // optional chaining
val ?? defaultVal // nullish coalescing
// Collections
new Set([1,2,3]) // unique values
new Map([[key, val]]) // any-type keys
Modern JavaScript is a joy to write when you know these features. Start incorporating them gradually — const/let, template literals, and arrow functions first, then destructuring and spread, then modules and classes. Within a few weeks, these patterns will feel completely natural.
Free Developer Tools
If you found this article helpful, check out DevToolkit — 40+ free browser-based developer tools with no signup required.
Popular tools: JSON Formatter · Regex Tester · JWT Decoder · Base64 Encoder
🛒 Get the DevToolkit Starter Kit on Gumroad — source code, deployment guide, and customization templates.
Top comments (0)