Modern JavaScript ES6+: Destructuring, Spread and Modules

By Engr. Henry Akpan

JavaScript Grew Up

JavaScript written before 2015 was full of var, long function keywords, string concatenation with +, and awkward workarounds. The 2015 edition of the language, called ES6 (ECMAScript 2015), and the yearly updates since then added features that make code shorter, safer and easier to read. Every modern framework, including React, Vue, Express and Next.js, uses them constantly, so you need to recognise them even to read the documentation. This tutorial covers modern JavaScript (ES6 and later): let and const, arrow functions, template literals, destructuring, default parameters, rest and spread, shorthand object syntax, optional chaining and nullish coalescing, array methods (map, filter, reduce, find, some and every), Object.entries, Set and Map, and ES modules. Each example runs in Node.js. It builds on the JavaScript basics tutorial.

let, const and Block Scope

Use const by default, let when a variable must be reassigned, and avoid the old var. Both let and const are block-scoped: they exist only inside the nearest pair of braces.

const school = "Unilag";
let count = 0;
count += 1;

if (true) {
  const inside = "only visible in this block";
  let count = 100;            // a different variable from the outer count
}
console.log(school, count);

const marks = [70, 80];
marks.push(90);               // allowed: the array itself can change
console.log(marks);

Output:

Unilag 1
[ 70, 80, 90 ]

const prevents reassigning the variable (marks = [] would fail), but the contents of a const array or object can still be changed.

Arrow Functions

Arrow functions are a shorter way to write functions. With a single expression, the braces and the return keyword can be left out, and the value is returned automatically.

function squareOld(x) { return x * x; }
const square = x => x * x;                  // one parameter: brackets optional
const add = (a, b) => a + b;
const greet = () => "Hello!";
const describe = (name, score) => {          // several statements: use braces and return
  const status = score >= 50 ? "passed" : "failed";
  return `${name} ${status}`;
};
const makeStudent = (name) => ({ name, level: 100 });   // wrap an object in brackets

console.log(squareOld(4), square(4), add(2, 3), greet());
console.log(describe("Ada", 72), makeStudent("Bayo"));

Output:

16 16 5 Hello!
Ada passed { name: 'Bayo', level: 100 }

Arrow functions do not have their own this: they use the this of the surrounding code. That makes them ideal for callbacks, but unsuitable as object methods that need this.

Template Literals

Strings in backticks can contain ${expression} placeholders and can span several lines.

const name = "Chidi";
const price = 2500;
const qty = 3;
console.log(`${name} bought ${qty} books for ₦${(price * qty).toLocaleString()}.`);

const receipt = `Item: textbook
Qty:  ${qty}
Total: ${price * qty}`;
console.log(receipt);

Output:

Chidi bought 3 books for ₦7,500.
Item: textbook
Qty:  3
Total: 7500

Destructuring

Destructuring unpacks values from arrays, or properties from objects, into separate variables in a single line.

const scores = [90, 72, 58, 45];
const [first, second, ...others] = scores;
console.log(first, second, others);

let a = 1, b = 2;
[a, b] = [b, a];                             // swap without a temporary variable
console.log(a, b);

const student = { name: "Dami", dept: "Maths", level: 200, address: { city: "Ibadan" } };
const { name, level, dept: department, address: { city } } = student;
console.log(name, level, department, city);

const { phone = "not given" } = student;     // default if the property is missing
console.log(phone);

Output:

90 72 [ 58, 45 ]
2 1
Dami 200 Maths Ibadan
not given

dept: department renames a property while unpacking it, and address: { city } reaches into a nested object. Destructuring is especially common in function parameters, as the next section shows.

Default Parameters and Destructured Arguments

const fee = (amount, discount = 0) => amount * (1 - discount);
console.log(fee(10000), fee(10000, 0.1));

function register({ name, course, units = 3 }) {
  return `${name} registered for ${course} (${units} units)`;
}
console.log(register({ name: "Efe", course: "CSC101" }));
console.log(register({ course: "PHY101", name: "Femi", units: 2 }));

Output:

10000 9000
Efe registered for CSC101 (3 units)
Femi registered for PHY101 (2 units)

Passing a single object of named options means the arguments can be given in any order, and new options can be added later without breaking existing calls. This pattern is used throughout JavaScript libraries.

Rest and Spread: the Three Dots

The ... syntax has two opposite jobs:

  • Rest collects several values into an array, as in function parameters or destructuring.
  • Spread expands an array or object out into individual values.
const total = (...nums) => nums.reduce((sum, n) => sum + n, 0);   // rest
console.log(total(1, 2, 3, 4));

const marks = [70, 85, 60];
console.log(Math.max(...marks));                       // spread into arguments

const more = [...marks, 95];                           // a copy with an extra item
console.log(more, marks.length);

const base = { name: "Ada", level: 100 };
const promoted = { ...base, level: 200, dept: "Physics" };   // copy and override
console.log(promoted, base.level);

Output:

10
85
[ 70, 85, 60, 95 ] 3
{ name: 'Ada', level: 200, dept: 'Physics' } 100

Spreading into a new array or object creates a copy without changing the original. This style of updating, called immutable updates, is central to React. The copy is shallow: nested objects are still shared.

Shorthand Object Syntax

const name = "Bola";
const cgpa = 4.21;
const key = "matric";

const record = {
  name,                       // same as name: name
  cgpa,
  [key]: "U2023/001",         // computed property name
  summary() {                 // method shorthand
    return `${this.name}: ${this.cgpa}`;
  },
};
console.log(record.matric, record.summary());

Output:

U2023/001 Bola: 4.21

Optional Chaining and Nullish Coalescing

Data from APIs often has missing parts. Reading user.address.city crashes when address is undefined. Two operators handle this neatly:

  • ?. (optional chaining) stops and returns undefined if the value on its left is null or undefined.
  • ?? (nullish coalescing) gives a fallback, but only for null or undefined. Unlike ||, it keeps genuine values such as 0 and "".
const users = [
  { name: "Ada", address: { city: "Lagos" }, score: 0 },
  { name: "Bayo" },
];
for (const u of users) {
  console.log(u.name, u.address?.city ?? "unknown city", u.score ?? "no score", u.score || "falsy!");
}
console.log(users[5]?.name, users[0].getAge?.());

Output:

Ada Lagos 0 falsy!
Bayo unknown city no score falsy!
undefined undefined

Notice the difference for Ada's score of 0: ?? keeps it, but || treats 0 as false and replaces it. ?.() calls a method only if it exists.

Array Methods

Modern JavaScript processes arrays with methods that take a function, instead of writing for loops by hand.

const students = [
  { name: "Ada", dept: "Physics", score: 72 },
  { name: "Bayo", dept: "Physics", score: 41 },
  { name: "Chidi", dept: "Maths", score: 90 },
  { name: "Dami", dept: "Maths", score: 58 },
];

const names = students.map(s => s.name);
const passed = students.filter(s => s.score >= 50);
const total = students.reduce((sum, s) => sum + s.score, 0);
const topMaths = students.find(s => s.dept === "Maths" && s.score > 80);

console.log(names);
console.log(passed.map(s => s.name), total / students.length);
console.log(topMaths.name, students.some(s => s.score < 45), students.every(s => s.score > 40));

const ranked = [...students].sort((x, y) => y.score - x.score).map(s => `${s.name}:${s.score}`);
console.log(ranked.join(", "));

const byDept = students.reduce((groups, s) => {
  (groups[s.dept] ??= []).push(s.name);
  return groups;
}, {});
console.log(byDept);

Output:

[ 'Ada', 'Bayo', 'Chidi', 'Dami' ]
[ 'Ada', 'Chidi', 'Dami' ] 65.25
Chidi true true
Chidi:90, Ada:72, Dami:58, Bayo:41
{ Physics: [ 'Ada', 'Bayo' ], Maths: [ 'Chidi', 'Dami' ] }
  • map transforms every item, and filter keeps the items that pass a test. Both return new arrays.
  • reduce combines all the items into one value, such as a total or a grouped object.
  • find returns the first matching item (or undefined), while some and every return true or false.
  • sort changes the array in place, so copy it first with spread. A compare function such as (x, y) => y.score - x.score sorts numbers correctly, here from highest to lowest.
  • ??= assigns a value only if the property is still null or undefined.

Object.entries, Set and Map

const prices = { rice: 75000, beans: 52000, garri: 30000 };
for (const [item, price] of Object.entries(prices)) {
  if (price > 40000) console.log(item, price);
}
console.log(Object.keys(prices).length, Object.values(prices).reduce((a, b) => a + b));

const visits = ["Lagos", "Abuja", "Lagos", "Kano", "Abuja"];
const unique = [...new Set(visits)];
console.log(unique, new Set(visits).has("Kano"));

const counts = new Map();
for (const city of visits) counts.set(city, (counts.get(city) ?? 0) + 1);
console.log([...counts], counts.get("Lagos"), counts.size);

Output:

rice 75000
beans 52000
3 157000
[ 'Lagos', 'Abuja', 'Kano' ] true
[ [ 'Lagos', 2 ], [ 'Abuja', 2 ], [ 'Kano', 1 ] ] 2 3

A Set stores unique values, and spreading a Set into an array is the quickest way to remove duplicates. A Map is a dictionary whose keys can be any type, which remembers the order of insertion and has a convenient size.

ES Modules: import and export

Modern JavaScript splits code into files called modules. A module exports what other files may use, and other files import it. In Node.js, use the .mjs extension (or set "type": "module" in package.json). Save this as grades.mjs:

// grades.mjs
export const PASS_MARK = 45;

export function grade(score) {
  if (score >= 70) return "A";
  if (score >= 60) return "B";
  if (score >= 50) return "C";
  return score >= PASS_MARK ? "D" : "F";
}

export default function average(scores) {
  return scores.reduce((a, b) => a + b, 0) / scores.length;
}

And this as main.mjs:

// main.mjs
import average, { grade, PASS_MARK } from "./grades.mjs";

const scores = [72, 58, 44];
console.log(scores.map(grade), average(scores).toFixed(1), PASS_MARK);

In a terminal:

$ node main.mjs
[ 'A', 'C', 'F' ] 58.0 45
  • Named exports (export const, export function) are imported inside braces, using the same names.
  • A module may have one default export, imported without braces under any name you choose.
  • Browsers and bundlers such as Vite use the same syntax. The older require and module.exports (CommonJS), covered in the Node.js basics tutorial, are still common in existing Node.js code.

Common Mistakes with Modern JavaScript

  • Thinking const makes objects unchangeable. It only stops reassignment. Use Object.freeze to prevent changes.
  • Returning an object from an arrow function without brackets: () => { a: 1 } is read as a function body. Write () => ({ a: 1 }).
  • Using || for defaults when 0, "" or false are valid values. Use ??.
  • Sorting numbers without a compare function: [10, 9, 1].sort() gives [1, 10, 9], because it sorts as text.
  • Expecting spread to deep-copy nested objects. It copies only one level.

Modern JavaScript: Practice Exercises

  1. Use destructuring to swap the first and last elements of an array of three items.
  2. Write an arrow function initials that turns "Ada Lovelace" into "A.L.".
  3. Given an array of products with name and price, use filter, map and reduce to find the total price of the items under ₦1,000.
  4. Count the words in a sentence with a Map, ignoring case.

Sample solutions:

let arr = ["x", "y", "z"];
[arr[0], arr[2]] = [arr[2], arr[0]];
console.log(arr);

const initials = full => full.split(" ").map(w => w[0].toUpperCase() + ".").join("");
console.log(initials("Ada Lovelace"), initials("chinua achebe"));

const products = [{ name: "pen", price: 150 }, { name: "bag", price: 4500 }, { name: "ruler", price: 300 }];
const cheapTotal = products.filter(p => p.price < 1000).map(p => p.price).reduce((a, b) => a + b, 0);
console.log(cheapTotal);

const wordCounts = new Map();
for (const w of "The cat and the hat".toLowerCase().split(" ")) {
  wordCounts.set(w, (wordCounts.get(w) ?? 0) + 1);
}
console.log(Object.fromEntries(wordCounts));

Output:

[ 'z', 'y', 'x' ]
A.L. C.A.
450
{ the: 2, cat: 1, and: 1, hat: 1 }

Conclusion

Modern JavaScript is shorter and safer than the older style. Use const and let for block-scoped variables, arrow functions for concise callbacks, and template literals for readable strings. Destructuring unpacks arrays and objects (including function parameters, with defaults), and rest and spread collect and expand values, making immutable copies easy. Optional chaining (?.) and nullish coalescing (??) handle missing data. Array methods such as map, filter, reduce, find, some and every replace manual loops, Set and Map store unique values and key–value pairs, and ES modules organise code with import and export.

Related Tutorials