Week 9: Modern ES6+ & Modules

A short, dense week: the syntax that shows up in essentially every modern JavaScript codebase, and the module system that lets code be split across files in the first place. None of this introduces new concepts so much as new, terser ways to write things you already know how to do the long way.

Phase 5 of 8 Week 9 of 14 ~2–3 Hours Hands-on Exercise Included

By the end of this week, you'll be able to

  • Destructure arrays and objects to pull values out in one line instead of many
  • Use optional chaining and nullish coalescing to guard against missing data safely
  • Split code across files with ES module import/export

1. Destructuring

Destructuring unpacks values out of an array or object into their own variables, in a single line, instead of accessing each one individually:

array destructuring
const coordinates = [10, 20, 30];

// Without destructuring
const x = coordinates[0];
const y = coordinates[1];

// With destructuring -- same result, one line
const [a, b, c] = coordinates;
console.log(a, b, c); // 10 20 30

// Skip elements you don't need with an empty slot
const [first, , third] = coordinates;
console.log(first, third); // 10 30
object destructuring
const user = { name: "Priya", age: 29, city: "Kolkata" };

// Pulls out properties BY NAME, in any order
const { age, name } = user;
console.log(name, age); // "Priya" 29

// Rename while destructuring, and provide a default for a missing property
const { name: userName, country = "India" } = user;
console.log(userName, country); // "Priya" "India"

Array destructuring matches by position — the first variable gets the first element, and so on. Object destructuring matches by property name instead, so the order the variables are listed in doesn't matter at all. Function parameters can be destructured directly too, which is extremely common for a function that takes one options object:

destructuring in a function signature
function greet({ name, greeting = "Hello" }) {
  return `${greeting}, ${name}!`;
}

greet({ name: "Ada" });               // "Hello, Ada!"
greet({ name: "Ada", greeting: "Hi" }); // "Hi, Ada!"

2. Spread & Rest, Revisited

Week 2 covered rest parameters, and Week 6 covered spread for copying arrays and objects — the same ... syntax also combines directly with destructuring, which is worth seeing explicitly:

rest inside destructuring
const [first, ...rest] = [1, 2, 3, 4];
console.log(first, rest); // 1 [2, 3, 4]

const { id, ...otherFields } = { id: 1, name: "Ada", age: 30 };
console.log(id, otherFields); // 1 { name: "Ada", age: 30 }
spread inside a function call
const numbers = [4, 2, 9, 1];

// Math.max normally takes individual arguments, not an array --
// spread expands the array into separate arguments
console.log(Math.max(...numbers)); // 9
console.log(Math.max(4, 2, 9, 1)); // 9 -- exactly equivalent

The direction is the useful thing to keep straight: rest gathers multiple individual values into one array/object (used when declaring — a parameter list or a destructuring pattern). Spread expands one array/object out into individual values (used when calling or building — a function call or a new array/object literal). Same three dots, opposite direction, and which one you're looking at is always determined by where the ... appears.

3. Optional Chaining & Nullish Coalescing

Two operators purpose-built for the single most common defensive-coding annoyance: safely reading a property that might not exist.

optional chaining: ?.
const user = { profile: { city: "Kolkata" } };
const guest = {};

console.log(user.profile?.city);   // "Kolkata"
console.log(guest.profile?.city);  // undefined -- NOT a crash

// Without ?., this would throw: "Cannot read properties of undefined"
// console.log(guest.profile.city);
nullish coalescing: ??
const settings = { volume: 0, brightness: null };

console.log(settings.volume ?? 50);     // 0 -- 0 is a real, intentional value
console.log(settings.volume || 50);     // 50 -- WRONG here, || treats 0 as falsy too

console.log(settings.brightness ?? 50); // 50 -- brightness is genuinely missing

?. stops evaluating and returns undefined immediately the moment it hits null or undefined, instead of throwing — chain several together (a?.b?.c) to safely walk arbitrarily deep into possibly-missing data. ?? provides a fallback only for null/undefined specifically — unlike ||, which also falls back on any falsy value (0, "", false), which is very rarely what's actually intended.

4. ES Modules

Everything so far in this course has lived in one file. A real project splits code across many files — ES modules are the standard, built-in way to share code between them with export and import:

math.js -- exporting
// Named exports: export as many of these as needed from one file
export function add(a, b) {
  return a + b;
}

export const PI = 3.14159;

// Default export: at most ONE per file, for the file's "main" thing
export default function multiply(a, b) {
  return a * b;
}
main.js -- importing
import multiply, { add, PI } from "./math.js";
// default export  ^          ^ named exports, in curly braces

console.log(add(2, 3));      // 5
console.log(multiply(2, 3)); // 6
console.log(PI);             // 3.14159

A file can have any number of named exports (imported with matching names inside {'{'} {'}'}) but at most one default export (imported under whatever name you choose, no braces). Each module also has its own private scope by default — a variable declared in math.js that isn't exported simply doesn't exist from main.js's point of view, which is exactly the "avoid leaking into a shared scope" problem Week 3's IIFEs used to solve by hand.

Running modules in the browser vs. Node

In the browser, a <script type="module"> tag enables import/export in that script. In Node, either name files .mjs, or set "type": "module" in package.json — Week 14's tooling section covers this setup directly.

5. Hands-on Exercise

Hands-on

Split a user-formatter into modules

Practice named/default exports, destructuring, and safe property access together.

Requirements:

  1. Create users.js that exports (as named exports) an array of at least 4 user objects, each with name, age, and an optional nested address: {'{'} city {'}'} (omit it on at least one user).
  2. Create format.js with a default-exported function formatUser(user) that destructures name and age from its parameter, uses ?. to safely read address?.city, and ?? to fall back to "Unknown" if the city is missing.
  3. In a main.js, import both modules and log formatUser(user) for every user in the array.
  4. Use array destructuring with rest (const [firstUser, ...otherUsers] = users) to separately log the first user and a count of the rest.
  5. Add one comment explaining why the user missing an address doesn't crash formatUser.
Hint

If running this directly with Node, remember either .mjs file extensions or "type": "module" in a package.json next to the files — plain .js files run as CommonJS by default and don't understand import/export.

6. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

Given const { age, name } = user;, does the order of age and name in the braces need to match the order of properties on user?

No. Object destructuring matches by property name, not position — {'{'} age, name {'}'} and {'{'} name, age {'}'} both pull the exact same values regardless of what order user's own properties happen to be in. (Array destructuring is the one that's positional — const [a, b] = arr genuinely does depend on order.)

Q2

What's the difference between rest and spread — they both use ...?

Rest gathers multiple values into one array/object, used when declaring something — a function parameter list or a destructuring pattern (const [first, ...rest] = arr). Spread expands one array/object out into individual values, used when calling or building something — a function call or a new array/object literal (Math.max(...numbers)). Which one applies is always determined by where the ... appears.

Q3

Why is settings.volume ?? 50 safer than settings.volume || 50 when volume could legitimately be 0?

|| falls back to its right-hand side for any falsy value, including 0, "", and false — so a genuinely intentional volume: 0 would incorrectly become 50. ?? only falls back when the left side is specifically null or undefined, leaving a real value like 0 untouched.

Q4

Can a single file have more than one default export?

No — at most one export default per file. A file can have any number of named exports alongside it, but the default export is meant to represent that module's single "main" thing, and JavaScript enforces that limit directly (a second export default in the same file is a syntax error).