Modern JavaScript: ES6+ Features Every Developer Should Know
Introduction
JavaScript has evolved significantly since ES6 (ECMAScript 2015), introducing powerful features that have transformed how we write modern web applications. These features not only make code more concise and readable but also provide better performance and developer experience.
This comprehensive guide covers the most important ES6+ features that every JavaScript developer should master. From arrow functions and destructuring to async/await and modules, you'll learn how to write more efficient, maintainable, and modern JavaScript code.
Arrow Functions
Arrow functions provide a more concise syntax for writing function expressions:
// Traditional function
function add(a, b) {
return a + b;
}
// Arrow function
const add = (a, b) => a + b;
// Single parameter (parentheses optional)
const square = x => x * x;
// No parameters
const greet = () => 'Hello World!';
// Multiple statements
const processData = (data) => {
const processed = data.map(item => item * 2);
return processed.filter(item => item > 10);
};
Key Differences:
- Arrow functions don't have their own `this` binding
- Use rest parameters instead
- No `prototype` property
- Must be declared before use
Common Use Cases:
// Array methods
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2);
const evens = numbers.filter(n => n % 2 === 0);
const sum = numbers.reduce((acc, n) => acc + n, 0);
// Event handlers
button.addEventListener('click', (event) => {
console.log('Button clicked!');
});
// Promises
fetch('/api/data')
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error(error));
Destructuring Assignment
Destructuring allows you to extract values from arrays or objects into distinct variables:
// Basic destructuring
const colors = ['red', 'green', 'blue'];
const [first, second, third] = colors;
console.log(first); // 'red'
console.log(second); // 'green'
console.log(third); // 'blue'
// Skip elements
const [first, , third] = colors;
console.log(first); // 'red'
console.log(third); // 'blue'
// Default values
const [first, second, third, fourth = 'yellow'] = colors;
console.log(fourth); // 'yellow'
// Rest operator
const [first, ...rest] = colors;
console.log(first); // 'red'
console.log(rest); // ['green', 'blue']
// Swap variables
let a = 1, b = 2;
[a, b] = [b, a];
console.log(a, b); // 2, 1
// Basic destructuring
const person = {
name: 'John',
age: 30,
city: 'New York'
};
const { name, age, city } = person;
console.log(name); // 'John'
console.log(age); // 30
console.log(city); // 'New York'
// Rename variables
const { name: fullName, age: years } = person;
console.log(fullName); // 'John'
console.log(years); // 30
// Default values
const { name, age, country = 'USA' } = person;
console.log(country); // 'USA'
// Nested destructuring
const user = {
id: 1,
profile: {
name: 'John',
email: 'john@example.com'
},
settings: {
theme: 'dark',
notifications: true
}
};
const {
id,
profile: { name, email },
settings: { theme }
} = user;
console.log(id, name, email, theme);
// Function parameters
function greetUser({ name, age = 18 }) {
console.log(`Hello ${name}, you are ${age} years old`);
}
greetUser({ name: 'John' }); // Hello John, you are 18 years old
Template Literals
Template literals provide an easy way to create strings with embedded expressions:
// Traditional string concatenation
const name = 'John';
const age = 30;
const message = 'Hello, my name is ' + name + ' and I am ' + age + ' years old';
// Template literal
const message = `Hello, my name is ${name} and I am ${age} years old`;
// Multi-line strings
const html = `
<div class="user-card">
<h2>${name}</h2>
<p>Age: ${age}</p>
</div>
`;
// Expressions in templates
const price = 19.99;
const tax = 0.08;
const total = `Total: $${(price * (1 + tax)).toFixed(2)}`;
console.log(total); // Total: $21.59
// Tagged templates
function highlight(strings, ...values) {
return strings.reduce((result, string, i) => {
const value = values[i] ? `<mark>${values[i]}</mark>` : '';
return result + string + value;
}, '');
}
const name = 'John';
const age = 30;
const highlighted = highlight`Hello ${name}, you are ${age} years old`;
console.log(highlighted); // Hello <mark>John</mark>, you are <mark>30</mark> years old
// SQL queries
const userId = 123;
const query = `
SELECT * FROM users
WHERE id = ${userId}
AND status = 'active'
`;
// API URLs
const baseUrl = 'https://api.example.com';
const endpoint = 'users';
const url = `${baseUrl}/${endpoint}/${userId}`;
// HTML templates
function createUserCard(user) {
return `
<div class="user-card" data-id="${user.id}">
<img src="${user.avatar}" alt="${user.name}">
<h3>${user.name}</h3>
<p>${user.email}</p>
<span class="status ${user.status}">${user.status}</span>
</div>
`;
}
Spread and Rest Operators
The spread (`...`) and rest operators provide powerful ways to work with arrays and objects:
// Array spreading
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2];
console.log(combined); // [1, 2, 3, 4, 5, 6]
// Copy arrays
const original = [1, 2, 3];
const copy = [...original];
copy.push(4);
console.log(original); // [1, 2, 3]
console.log(copy); // [1, 2, 3, 4]
// Function arguments
const numbers = [1, 2, 3, 4, 5];
const max = Math.max(...numbers);
console.log(max); // 5
// Object spreading
const obj1 = { a: 1, b: 2 };
const obj2 = { c: 3, d: 4 };
const combined = { ...obj1, ...obj2 };
console.log(combined); // { a: 1, b: 2, c: 3, d: 4 }
// Object copying and updating
const user = { name: 'John', age: 30, city: 'NYC' };
const updatedUser = { ...user, age: 31, country: 'USA' };
console.log(updatedUser); // { name: 'John', age: 31, city: 'NYC', country: 'USA' }
// Shallow copy
const original = { a: 1, b: { c: 2 } };
const copy = { ...original };
copy.b.c = 3;
console.log(original.b.c); // 3 (shallow copy)
// Function parameters
function sum(...numbers) {
return numbers.reduce((acc, num) => acc + num, 0);
}
console.log(sum(1, 2, 3, 4, 5)); // 15
// Array destructuring
const [first, second, ...rest] = [1, 2, 3, 4, 5];
console.log(first); // 1
console.log(second); // 2
console.log(rest); // [3, 4, 5]
// Object destructuring
const { name, age, ...otherProps } = {
name: 'John',
age: 30,
city: 'NYC',
country: 'USA'
};
console.log(name); // 'John'
console.log(age); // 30
console.log(otherProps); // { city: 'NYC', country: 'USA' }
// Collecting arguments
function logMessage(level, ...messages) {
console.log(`[${level}]`, ...messages);
}
logMessage('INFO', 'User logged in', 'Session started');
// [INFO] User logged in Session started
Classes and Inheritance
ES6 introduced class syntax for object-oriented programming:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
greet() {
return `Hello, my name is ${this.name}`;
}
get info() {
return `${this.name} is ${this.age} years old`;
}
set age(newAge) {
if (newAge < 0) {
throw new Error('Age cannot be negative');
}
this._age = newAge;
}
get age() {
return this._age;
}
}
const person = new Person('John', 30);
console.log(person.greet()); // Hello, my name is John
console.log(person.info); // John is 30 years old
class Student extends Person {
constructor(name, age, studentId) {
super(name, age); // Call parent constructor
this.studentId = studentId;
}
greet() {
return `${super.greet()} and I am a student`;
}
study() {
return `${this.name} is studying`;
}
}
const student = new Student('Alice', 20, 'S12345');
console.log(student.greet()); // Hello, my name is Alice and I am a student
console.log(student.study()); // Alice is studying
class MathUtils {
static PI = 3.14159;
static add(a, b) {
return a + b;
}
static multiply(a, b) {
return a * b;
}
static circleArea(radius) {
return this.PI * radius * radius;
}
}
console.log(MathUtils.add(5, 3)); // 8
console.log(MathUtils.multiply(4, 6)); // 24
console.log(MathUtils.circleArea(5)); // 78.53975
class BankAccount {
#balance = 0; // Private field
#accountNumber; // Private field
constructor(accountNumber, initialBalance = 0) {
this.#accountNumber = accountNumber;
this.#balance = initialBalance;
}
deposit(amount) {
if (amount > 0) {
this.#balance += amount;
}
}
withdraw(amount) {
if (amount > 0 && amount <= this.#balance) {
this.#balance -= amount;
}
}
getBalance() {
return this.#balance;
}
}
const account = new BankAccount('12345', 1000);
account.deposit(500);
console.log(account.getBalance()); // 1500
// console.log(account.#balance); // SyntaxError: Private field '#balance' must be declared in an enclosing class
Modules (ES6 Modules)
ES6 modules provide a standardized way to organize and share code:
// 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;
}
// Default export
export default function subtract(a, b) {
return a - b;
}
// Alternative export syntax
const divide = (a, b) => a / b;
export { divide };
// Export multiple items
const operations = {
add,
subtract,
multiply,
divide
};
export { operations };
// Import named exports
import { add, multiply, PI } from './math.js';
// Import with alias
import { add as addition, multiply as multiplication } from './math.js';
// Import default export
import subtract from './math.js';
// Import all named exports
import * as math from './math.js';
console.log(math.add(5, 3)); // 8
// Import default and named exports
import subtract, { add, multiply } from './math.js';
// Dynamic imports
async function loadModule() {
const { add, multiply } = await import('./math.js');
console.log(add(5, 3)); // 8
}
// user.js
export class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
greet() {
return `Hello, I'm ${this.name}`;
}
}
export function createUser(name, email) {
return new User(name, email);
}
export const DEFAULT_USER = new User('Guest', 'guest@example.com');
// utils.js
export const formatDate = (date) => {
return date.toLocaleDateString();
};
export const formatCurrency = (amount) => {
return `$${amount.toFixed(2)}`;
};
// main.js
import { User, createUser, DEFAULT_USER } from './user.js';
import { formatDate, formatCurrency } from './utils.js';
const user = createUser('John', 'john@example.com');
console.log(user.greet()); // Hello, I'm John
const today = new Date();
console.log(formatDate(today)); // 12/25/2023
console.log(formatCurrency(19.99)); // $19.99
Promises and Async/Await
Promises and async/await provide elegant ways to handle asynchronous operations:
// Creating a Promise
function fetchData(url) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onload = () => {
if (xhr.status === 200) {
resolve(JSON.parse(xhr.responseText));
} else {
reject(new Error(`HTTP ${xhr.status}`));
}
};
xhr.onerror = () => reject(new Error('Network error'));
xhr.send();
});
}
// Using Promises
fetchData('/api/users')
.then(data => {
console.log('Users:', data);
return fetchData('/api/posts');
})
.then(posts => {
console.log('Posts:', posts);
})
.catch(error => {
console.error('Error:', error.message);
});
// Promise.all - Wait for all promises
Promise.all([
fetchData('/api/users'),
fetchData('/api/posts'),
fetchData('/api/comments')
])
.then(([users, posts, comments]) => {
console.log('All data loaded:', { users, posts, comments });
})
.catch(error => {
console.error('One or more requests failed:', error);
});
// Promise.race - First promise to resolve
Promise.race([
fetchData('/api/fast-endpoint'),
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 5000))
])
.then(data => {
console.log('Fast response:', data);
})
.catch(error => {
console.error('Request failed or timed out:', error);
});
// Async function
async function loadUserData(userId) {
try {
const user = await fetchData(`/api/users/${userId}`);
const posts = await fetchData(`/api/users/${userId}/posts`);
const comments = await fetchData(`/api/users/${userId}/comments`);
return {
user,
posts,
comments
};
} catch (error) {
console.error('Failed to load user data:', error);
throw error;
}
}
// Using async/await
loadUserData(123)
.then(data => {
console.log('User data loaded:', data);
})
.catch(error => {
console.error('Error loading user data:', error);
});
// Parallel async operations
async function loadAllData() {
try {
const [users, posts, comments] = await Promise.all([
fetchData('/api/users'),
fetchData('/api/posts'),
fetchData('/api/comments')
]);
return { users, posts, comments };
} catch (error) {
console.error('Failed to load data:', error);
throw error;
}
}
// Async iteration
async function processUsers() {
const users = await fetchData('/api/users');
for (const user of users) {
try {
const userDetails = await fetchData(`/api/users/${user.id}/details`);
console.log('User details:', userDetails);
} catch (error) {
console.error(`Failed to load details for user ${user.id}:`, error);
}
}
}
Map, Set, and WeakMap/WeakSet
ES6 introduced new data structures for better data management:
// Creating a Map
const userMap = new Map();
// Adding key-value pairs
userMap.set('user1', { name: 'John', age: 30 });
userMap.set('user2', { name: 'Alice', age: 25 });
// Getting values
console.log(userMap.get('user1')); // { name: 'John', age: 30 }
// Checking if key exists
console.log(userMap.has('user1')); // true
// Getting size
console.log(userMap.size); // 2
// Deleting entries
userMap.delete('user1');
console.log(userMap.size); // 1
// Iterating over Map
for (const [key, value] of userMap) {
console.log(`${key}: ${value.name}`);
}
// Converting to Array
const entries = Array.from(userMap.entries());
const keys = Array.from(userMap.keys());
const values = Array.from(userMap.values());
// Map vs Object
const obj = {};
const map = new Map();
// Keys can be any type in Map
map.set(1, 'one');
map.set('1', 'string one');
map.set({}, 'object key');
console.log(map.get(1)); // 'one'
console.log(map.get('1')); // 'string one'
console.log(map.get({})); // undefined (different object)
// Creating a Set
const numbers = new Set([1, 2, 3, 4, 5]);
const uniqueNames = new Set(['John', 'Alice', 'John', 'Bob']);
console.log(uniqueNames); // Set(3) { 'John', 'Alice', 'Bob' }
// Adding values
numbers.add(6);
numbers.add(1); // Won't add duplicate
// Checking if value exists
console.log(numbers.has(3)); // true
// Getting size
console.log(numbers.size); // 6
// Deleting values
numbers.delete(3);
console.log(numbers.has(3)); // false
// Iterating over Set
for (const number of numbers) {
console.log(number);
}
// Set operations
const set1 = new Set([1, 2, 3]);
const set2 = new Set([2, 3, 4]);
// Union
const union = new Set([...set1, ...set2]);
console.log(union); // Set(4) { 1, 2, 3, 4 }
// Intersection
const intersection = new Set([...set1].filter(x => set2.has(x)));
console.log(intersection); // Set(2) { 2, 3 }
// Difference
const difference = new Set([...set1].filter(x => !set2.has(x)));
console.log(difference); // Set(1) { 1 }
// WeakMap - keys must be objects
const weakMap = new WeakMap();
const obj1 = {};
const obj2 = {};
weakMap.set(obj1, 'value1');
weakMap.set(obj2, 'value2');
console.log(weakMap.get(obj1)); // 'value1'
// WeakSet - values must be objects
const weakSet = new WeakSet();
const obj3 = {};
const obj4 = {};
weakSet.add(obj3);
weakSet.add(obj4);
console.log(weakSet.has(obj3)); // true
// WeakMap/WeakSet are not enumerable
// They don't have size property
// They don't have clear() method
// They allow garbage collection of keys/values
Symbols
Symbols are unique, immutable primitive values that can be used as object keys:
// Basic symbol
const sym1 = Symbol();
const sym2 = Symbol('description');
const sym3 = Symbol('description');
console.log(sym1); // Symbol()
console.log(sym2); // Symbol(description)
console.log(sym2 === sym3); // false (each symbol is unique)
// Global symbol registry
const globalSym1 = Symbol.for('key');
const globalSym2 = Symbol.for('key');
console.log(globalSym1 === globalSym2); // true
// Getting key from global symbol
console.log(Symbol.keyFor(globalSym1)); // 'key'
// Creating symbols for object properties
const id = Symbol('id');
const name = Symbol('name');
const user = {
[id]: 123,
[name]: 'John',
age: 30
};
console.log(user[id]); // 123
console.log(user[name]); // 'John'
console.log(user.age); // 30
// Symbols are not enumerable by default
for (const key in user) {
console.log(key); // Only 'age'
}
// Getting symbol properties
const symbolKeys = Object.getOwnPropertySymbols(user);
console.log(symbolKeys); // [Symbol(id), Symbol(name)]
// All properties (including symbols)
const allKeys = Reflect.ownKeys(user);
console.log(allKeys); // ['age', Symbol(id), Symbol(name)]
// Symbol.iterator - makes object iterable
const iterable = {
data: [1, 2, 3, 4, 5],
[Symbol.iterator]() {
let index = 0;
const data = this.data;
return {
next() {
if (index < data.length) {
return { value: data[index++], done: false };
} else {
return { done: true };
}
}
};
}
};
// Now we can use for...of
for (const value of iterable) {
console.log(value); // 1, 2, 3, 4, 5
}
// Symbol.toPrimitive - custom type conversion
const customObject = {
value: 42,
[Symbol.toPrimitive](hint) {
if (hint === 'number') {
return this.value;
} else if (hint === 'string') {
return `Value: ${this.value}`;
} else {
return this.value;
}
}
};
console.log(+customObject); // 42 (number)
console.log(`${customObject}`); // 'Value: 42' (string)
console.log(customObject + 1); // 43 (default)