Modern ES6+ Features
Why This Matters
ES6 (2015) and the annual releases since have added features that have fundamentally changed how JavaScript is written. Understanding these lets you read modern code fluently and write cleaner, more expressive JavaScript.
Spread Operator (...)
The spread operator expands an iterable (array, string, object) into individual elements.
With Arrays
const a = [1, 2, 3];
const b = [4, 5, 6];
// Combine arrays
const combined = [...a, ...b]; // [1, 2, 3, 4, 5, 6]
// Copy an array (shallow)
const copy = [...a]; // [1, 2, 3]
// Pass array elements as function arguments
Math.max(...a); // 3
With Objects
const user = { name: "Rizwan", age: 25 };
const location = { city: "Lahore", country: "Pakistan" };
// Merge objects
const fullProfile = { ...user, ...location };
// { name: "Rizwan", age: 25, city: "Lahore", country: "Pakistan" }
// Clone with overrides
const updated = { ...user, age: 26 };
// { name: "Rizwan", age: 26 }
Later properties override earlier ones when keys conflict:
const base = { color: "red", size: "M" };
const override = { ...base, color: "blue" }; // color is now "blue"
Rest Parameters (...)
Same syntax, opposite purpose: rest collects multiple values into an array.
// Collect remaining function arguments
function sum(first, ...rest) {
return rest.reduce((total, n) => total + n, first);
}
sum(1, 2, 3, 4); // 10
// Collect remaining object properties
const { name, age, ...others } = { name: "Rizwan", age: 25, city: "Lahore", role: "dev" };
// name = "Rizwan", age = 25, others = { city: "Lahore", role: "dev" }
Optional Chaining (?.)
Safely access nested properties without crashing when an intermediate value is null or undefined.
Without optional chaining (verbose and error-prone):
const city = user && user.address && user.address.city;
With optional chaining:
const city = user?.address?.city; // undefined if any part is nullish
Works with method calls and array access too:
user?.getFullName?.(); // call method only if it exists
data?.items?.[0]?.title; // safe array index access
A common real-world example — reading an API response:
const token = response?.data?.auth?.token;
const firstTag = post?.tags?.[0];
Nullish Coalescing (??)
Return the right-hand value only if the left side is null or undefined (not just falsy).
const name = user.name ?? "Anonymous"; // only falls back if name is null/undefined
Compare with || (OR), which falls back on any falsy value:
const countOr = 0 || "default"; // "default" — probably not what you want!
const countNc = 0 ?? "default"; // 0 — correct, 0 is a valid value
Use ?? when 0, false, or "" are valid values you don't want to replace.
Optional Chaining + Nullish Coalescing Together
A very common pattern:
const displayName = user?.profile?.displayName ?? "Anonymous User";
const port = config?.server?.port ?? 3000;
Map and Set
Set — Collection of Unique Values
const set = new Set([1, 2, 3, 2, 1]);
console.log(set); // Set {1, 2, 3} — duplicates removed
set.add(4);
set.has(3); // true
set.delete(1);
set.size; // 3
// Most common use — remove duplicates from an array
const unique = [...new Set([1, 2, 2, 3, 3, 4])]; // [1, 2, 3, 4]
Map — Key-Value Pairs with Any Key Type
Unlike plain objects, Map keys can be any type (objects, functions, etc.) and insertion order is preserved.
const map = new Map();
map.set("name", "Rizwan");
map.set(42, "the answer");
map.set(true, "boolean key");
map.get("name"); // "Rizwan"
map.has(42); // true
map.delete(true);
map.size; // 2
// Iterate
map.forEach((value, key) => console.log(key, value));
for (const [key, value] of map) { ... }
When to use Map over a plain object:
- Keys that aren't strings
- Need to know the size quickly (
map.size) - Frequent additions and deletions
- Need ordered iteration
Short-Circuit Evaluation
// && returns the first falsy value, or the last value if all are truthy
const result = isLoggedIn && <Dashboard />; // renders Dashboard or false
// || returns the first truthy value, or the last value
const name = input || "default";
// Common pattern: conditional execution
isAdmin && deleteUser(id); // only runs if isAdmin is true
Tagged Template Literals
You already know template literals:
const greeting = `Hello, ${name}!`;
For multi-line strings:
const query = `
SELECT *
FROM users
WHERE id = ${userId}
`;
The example above is for illustration. In production, always use parameterized queries — never interpolate user input into SQL strings directly, as it leads to SQL injection.
Object.entries, Object.keys, Object.values
Iterate over objects like arrays:
const scores = { alice: 95, bob: 87, carol: 92 };
Object.keys(scores); // ["alice", "bob", "carol"]
Object.values(scores); // [95, 87, 92]
Object.entries(scores); // [["alice", 95], ["bob", 87], ["carol", 92]]
// Find the highest scorer
const [topName, topScore] = Object.entries(scores)
.sort(([, a], [, b]) => b - a)[0];
// topName = "alice", topScore = 95
Transform an object:
const doubled = Object.fromEntries(
Object.entries(scores).map(([key, val]) => [key, val * 2])
);
// { alice: 190, bob: 174, carol: 184 }
Quick Reference
| Feature | Syntax | Use for |
|---|---|---|
| Spread (array) | [...arr] | Copy/merge arrays, pass as args |
| Spread (object) | {...obj} | Copy/merge objects |
| Rest | (...args) | Collect remaining args/properties |
| Optional chaining | a?.b?.c | Safe nested property access |
| Nullish coalescing | a ?? b | Default only for null/undefined |
| Set | new Set(arr) | Unique values, deduplication |
| Map | new Map() | Key-value with non-string keys |