let and const (Recap)
ES6 introduced let and const as replacements for var.
If you have been following the previous tutorials, you are already using them.
Here is a quick summary of why they matter.
// var — function-scoped, hoisted, can be redeclared (avoid)
var x = 1;
var x = 2; // No error — this is confusing
// let — block-scoped, can be reassigned, cannot be redeclared
let y = 1;
y = 2; // OK — reassignment is fine
// let y = 3; // Error! Cannot redeclare in same scope
// const — block-scoped, cannot be reassigned
const z = 1;
// z = 2; // Error! Cannot reassign
// const with objects/arrays — the reference is constant, not the contents
const user = { name: "Alice" };
user.name = "Bob"; // OK — modifying properties is fine
// user = {}; // Error! Cannot reassign the variable
const numbers = [1, 2, 3];
numbers.push(4); // OK — modifying the array is fine
// numbers = []; // Error! Cannot reassign
Rule of thumb: use const by default. Switch to let only when
you need to reassign. Never use var.
Arrow Functions (Recap)
Arrow functions provide a shorter syntax for writing functions. They are especially useful for callbacks and inline functions.
// Traditional function
function add(a, b) {
return a + b;
}
// Arrow function
const add = (a, b) => {
return a + b;
};
// Short form — implicit return (no curly braces, no return keyword)
const add = (a, b) => a + b;
// Single parameter — parentheses are optional
const double = n => n * 2;
// No parameters — empty parentheses required
const greet = () => "Hello!";
// Returning an object — wrap in parentheses
const makeUser = (name) => ({ name: name, role: "user" });
// Perfect for callbacks
const numbers = [1, 2, 3, 4, 5];
const doubled = numbers.map(n => n * 2); // [2, 4, 6, 8, 10]
const evens = numbers.filter(n => n % 2 === 0); // [2, 4]
const sum = numbers.reduce((acc, n) => acc + n, 0); // 15
Template Literals
Template literals use backticks instead of quotes and support string interpolation and multi-line strings.
const name = "Alice";
const age = 30;
// Old way — string concatenation
const message = "Hello, " + name + "! You are " + age + " years old.";
// Template literal — much cleaner
const message = `Hello, ${name}! You are ${age} years old.`;
// Expressions inside ${}
const price = 19.99;
const tax = 0.08;
console.log(`Total: $${(price * (1 + tax)).toFixed(2)}`); // Total: $21.59
// Multi-line strings
const html = `
<div class="card">
<h2>${name}</h2>
<p>Age: ${age}</p>
</div>
`;
// Conditional expressions
const status = `User is ${age >= 18 ? "an adult" : "a minor"}`;
// Function calls inside templates
const items = ["apple", "banana", "cherry"];
console.log(`Items: ${items.join(", ")}`); // Items: apple, banana, cherry
Tagged Templates
// Tagged templates let you process template literals with a function
function highlight(strings, ...values) {
return strings.reduce((result, str, i) => {
const value = values[i] !== undefined ? `<strong>${values[i]}</strong>` : "";
return result + str + value;
}, "");
}
const name = "Alice";
const role = "admin";
const html = highlight`User ${name} has ${role} access`;
// "User <strong>Alice</strong> has <strong>admin</strong> access"
Destructuring
Destructuring lets you unpack values from arrays or properties from objects into distinct variables in a single statement.
Object Destructuring
const user = {
name: "Alice",
age: 30,
email: "alice@example.com",
address: {
city: "New York",
country: "USA"
}
};
// Without destructuring
const name = user.name;
const age = user.age;
// With destructuring
const { name, age, email } = user;
console.log(name); // "Alice"
console.log(age); // 30
console.log(email); // "alice@example.com"
// Rename variables
const { name: userName, email: userEmail } = user;
console.log(userName); // "Alice"
// Default values
const { name, role = "user" } = user;
console.log(role); // "user" (doesn't exist on object, uses default)
// Nested destructuring
const { address: { city, country } } = user;
console.log(city); // "New York"
console.log(country); // "USA"
// In function parameters
function greet({ name, age }) {
console.log(`Hello ${name}, you are ${age}`);
}
greet(user); // Hello Alice, you are 30
Array Destructuring
const colors = ["red", "green", "blue", "yellow"];
// Basic destructuring
const [first, second, third] = colors;
console.log(first); // "red"
console.log(second); // "green"
// Skip elements
const [, , thirdColor] = colors;
console.log(thirdColor); // "blue"
// Rest element (collect remaining items)
const [primary, ...others] = colors;
console.log(primary); // "red"
console.log(others); // ["green", "blue", "yellow"]
// Default values
const [a, b, c, d, e = "purple"] = colors;
console.log(e); // "purple"
// Swap variables without a temp variable
let x = 1;
let y = 2;
[x, y] = [y, x];
console.log(x); // 2
console.log(y); // 1
// Destructuring function return values
function getCoordinates() {
return [40.7128, -74.0060];
}
const [lat, lng] = getCoordinates();
Spread and Rest Operators
The ... syntax serves two purposes: spreading (expanding) and
resting (collecting).
Spread Operator (Expanding)
// Spread arrays
const fruits = ["apple", "banana"];
const moreFruits = ["cherry", ...fruits, "date"];
// ["cherry", "apple", "banana", "date"]
// Copy an array (shallow)
const original = [1, 2, 3];
const copy = [...original];
copy.push(4);
console.log(original); // [1, 2, 3] — unchanged
// Merge arrays
const all = [...fruits, ...moreFruits];
// Spread objects
const defaults = { theme: "dark", language: "en", fontSize: 14 };
const userPrefs = { language: "es", fontSize: 16 };
const settings = { ...defaults, ...userPrefs };
// { theme: "dark", language: "es", fontSize: 16 }
// Later properties override earlier ones
// Copy an object (shallow)
const userCopy = { ...user };
// Add/override properties
const updatedUser = { ...user, age: 31, role: "admin" };
// Spread into function arguments
const numbers = [5, 3, 8, 1, 9];
const max = Math.max(...numbers); // 9
Rest Operator (Collecting)
// Rest in function parameters — collect all arguments
function sum(...numbers) {
return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3)); // 6
console.log(sum(1, 2, 3, 4)); // 10
// Rest with other parameters
function logFirst(first, ...rest) {
console.log("First:", first);
console.log("Rest:", rest);
}
logFirst("a", "b", "c"); // First: a, Rest: ["b", "c"]
// Rest in object destructuring
const { name, ...otherProps } = user;
console.log(name); // "Alice"
console.log(otherProps); // { age: 30, email: "...", address: {...} }
// Rest in array destructuring
const [head, ...tail] = [1, 2, 3, 4, 5];
console.log(head); // 1
console.log(tail); // [2, 3, 4, 5]
Default Parameters
Functions can have default values for parameters. If the caller does not provide
an argument (or passes undefined), the default is used.
// Basic defaults
function greet(name = "World") {
return `Hello, ${name}!`;
}
console.log(greet()); // "Hello, World!"
console.log(greet("Alice")); // "Hello, Alice!"
// Defaults can use other parameters
function createUser(name, role = "user", id = Date.now()) {
return { name, role, id };
}
// Defaults with destructuring
function configure({ theme = "dark", language = "en", debug = false } = {}) {
console.log(`Theme: ${theme}, Language: ${language}, Debug: ${debug}`);
}
configure(); // Uses all defaults
configure({ theme: "light" }); // Override just one
configure({ debug: true }); // Override just one
// Defaults with expressions
function fetchData(url, timeout = 5000, retries = 3) {
console.log(`Fetching ${url} (timeout: ${timeout}ms, retries: ${retries})`);
}
fetchData("/api/users"); // timeout: 5000, retries: 3
fetchData("/api/users", 10000); // timeout: 10000, retries: 3
fetchData("/api/users", undefined, 1); // timeout: 5000, retries: 1
Optional Chaining (?.)
Optional chaining lets you safely access deeply nested properties without
checking each level for null or undefined.
const user = {
name: "Alice",
address: {
city: "New York"
}
};
// Without optional chaining — verbose null checks
const zipCode = user && user.address && user.address.zipCode;
// With optional chaining — clean and safe
const zipCode = user?.address?.zipCode;
console.log(zipCode); // undefined (no error thrown)
// Without it, this would throw: "Cannot read property of undefined"
const street = user?.address?.street?.name; // undefined, not an error
// Works with methods
const result = user?.getProfile?.(); // undefined if getProfile doesn't exist
// Works with arrays
const firstOrder = user?.orders?.[0]; // undefined if orders doesn't exist
// Works with function calls
const callback = undefined;
callback?.(); // Does nothing instead of throwing an error
// Practical example
function displayCity(user) {
const city = user?.address?.city ?? "Unknown";
console.log(`City: ${city}`);
}
displayCity({ name: "Alice", address: { city: "NYC" } }); // City: NYC
displayCity({ name: "Bob" }); // City: Unknown
displayCity(null); // City: Unknown
Nullish Coalescing (??)
The nullish coalescing operator ?? returns the right-hand value only when
the left-hand value is null or undefined. This is different
from ||, which also treats 0, "", and
false as falsy.
// || treats 0, "", and false as falsy
const count = 0;
console.log(count || 10); // 10 (0 is falsy!)
console.log(count ?? 10); // 0 (0 is NOT null/undefined)
const name = "";
console.log(name || "Anonymous"); // "Anonymous" ("" is falsy!)
console.log(name ?? "Anonymous"); // "" ("" is NOT null/undefined)
const debug = false;
console.log(debug || true); // true (false is falsy!)
console.log(debug ?? true); // false (false is NOT null/undefined)
// Only null and undefined trigger the fallback with ??
console.log(null ?? "default"); // "default"
console.log(undefined ?? "default"); // "default"
console.log(0 ?? "default"); // 0
console.log("" ?? "default"); // ""
console.log(false ?? "default"); // false
// Practical: setting config with safe defaults
function configure(options) {
const timeout = options.timeout ?? 5000; // 0 is a valid timeout
const retries = options.retries ?? 3; // 0 retries means "don't retry"
const verbose = options.verbose ?? false;
return { timeout, retries, verbose };
}
configure({ timeout: 0, retries: 0 });
// { timeout: 0, retries: 0, verbose: false }
// With || this would incorrectly become { timeout: 5000, retries: 3, verbose: false }
Map and Set
ES6 introduced Map and Set as alternatives to plain objects
and arrays for specific use cases.
Map
A Map is a collection of key-value pairs where keys can be any type
(not just strings like regular objects).
// Creating a Map
const userRoles = new Map();
// Setting values
userRoles.set("alice", "admin");
userRoles.set("bob", "editor");
userRoles.set("charlie", "viewer");
// Getting values
console.log(userRoles.get("alice")); // "admin"
console.log(userRoles.get("dave")); // undefined
// Checking existence
console.log(userRoles.has("bob")); // true
// Size
console.log(userRoles.size); // 3
// Deleting
userRoles.delete("charlie");
// Initialize with entries
const config = new Map([
["theme", "dark"],
["language", "en"],
["fontSize", 14]
]);
// Keys can be ANY type — even objects
const objectKey = { id: 1 };
const scores = new Map();
scores.set(objectKey, 95);
console.log(scores.get(objectKey)); // 95
// Iterating
userRoles.forEach((value, key) => {
console.log(`${key}: ${value}`);
});
// Using for...of
for (const [key, value] of userRoles) {
console.log(`${key} is a ${value}`);
}
// Convert to array
const entries = [...userRoles]; // [["alice","admin"], ["bob","editor"]]
const keys = [...userRoles.keys()]; // ["alice", "bob"]
const values = [...userRoles.values()]; // ["admin", "editor"]
Set
A Set is a collection of unique values. Duplicates are automatically removed.
// Creating a Set
const tags = new Set();
// Adding values
tags.add("javascript");
tags.add("tutorial");
tags.add("javascript"); // Duplicate — ignored
console.log(tags.size); // 2
// Checking existence
console.log(tags.has("javascript")); // true
// Deleting
tags.delete("tutorial");
// Initialize with an array
const uniqueNumbers = new Set([1, 2, 3, 2, 1, 4]);
console.log([...uniqueNumbers]); // [1, 2, 3, 4]
// Common use: remove duplicates from an array
const items = ["apple", "banana", "apple", "cherry", "banana"];
const unique = [...new Set(items)];
console.log(unique); // ["apple", "banana", "cherry"]
// Iterating
tags.forEach(tag => console.log(tag));
for (const tag of tags) {
console.log(tag);
}
// Set operations (manual)
const setA = new Set([1, 2, 3, 4]);
const setB = new Set([3, 4, 5, 6]);
// Union
const union = new Set([...setA, ...setB]); // {1,2,3,4,5,6}
// Intersection
const intersection = new Set([...setA].filter(x => setB.has(x))); // {3,4}
// Difference
const difference = new Set([...setA].filter(x => !setB.has(x))); // {1,2}
All features in this tutorial are supported in every modern browser (Chrome, Firefox, Safari, Edge) and Node.js 14+. If you need to support Internet Explorer or very old browsers, you will need a transpiler like Babel to convert modern syntax into compatible code. For most projects today, you can safely use all ES6+ features.
Practical Examples
Here are some real-world patterns that combine multiple ES6+ features together.
// API response processing using destructuring, spread, and arrow functions
async function loadUsers() {
const response = await fetch("/api/users");
const data = await response.json();
// Destructure and transform each user
const users = data.map(({ id, name, email, address }) => ({
id,
name,
email,
city: address?.city ?? "Unknown"
}));
return users;
}
// Configuration merger with defaults
function createApp(userConfig = {}) {
const defaults = {
theme: "dark",
language: "en",
debug: false,
api: {
baseUrl: "/api",
timeout: 5000
}
};
// Deep merge using spread
const config = {
...defaults,
...userConfig,
api: {
...defaults.api,
...userConfig?.api
}
};
return config;
}
const app = createApp({ theme: "light", api: { timeout: 10000 } });
// { theme: "light", language: "en", debug: false,
// api: { baseUrl: "/api", timeout: 10000 } }
// Event handler using template literals and destructuring
document.querySelector("#search").addEventListener("input", ({ target: { value } }) => {
const results = items.filter(item =>
item.name.toLowerCase().includes(value.toLowerCase())
);
renderResults(results);
});
// Unique tag counter using Map and Set
function analyzeTags(posts) {
const tagCount = new Map();
const allTags = new Set();
for (const { tags = [] } of posts) {
for (const tag of tags) {
allTags.add(tag);
tagCount.set(tag, (tagCount.get(tag) ?? 0) + 1);
}
}
// Sort by count descending
const sorted = [...tagCount.entries()]
.sort(([, a], [, b]) => b - a);
return {
uniqueCount: allTags.size,
topTags: sorted.slice(0, 5)
};
}
Summary
- Use
constby default,letwhen reassignment is needed, nevervar - Arrow functions (
=>) provide concise syntax, especially for callbacks - Template literals (
`...${}`) replace string concatenation with cleaner interpolation - Destructuring unpacks values from objects and arrays into variables
- Spread (
...) expands arrays/objects; rest (...) collects remaining items - Default parameters eliminate the need for manual
undefinedchecks - Optional chaining (
?.) safely accesses nested properties without null checks - Nullish coalescing (
??) provides defaults only fornull/undefined, not all falsy values MapandSetoffer specialized collections for key-value pairs and unique values
You now know the essential ES6+ features used in every modern codebase. Next up: Modules and Package Management — learn how to organize your code into reusable modules and leverage the npm ecosystem.