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}
💡
Browser compatibility

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 const by default, let when reassignment is needed, never var
  • 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 undefined checks
  • Optional chaining (?.) safely accesses nested properties without null checks
  • Nullish coalescing (??) provides defaults only for null/undefined, not all falsy values
  • Map and Set offer specialized collections for key-value pairs and unique values
🎉
Modern JavaScript unlocked!

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.