4.9: ES6+ Modern Features

Learn modern JavaScript best practices including ES6+ features, code organization, and debugging techniques. Discover destructuring, template literals, modules, and how to write clean, maintainable JavaScript code.

1. Destructuring (Recap & Advanced)

Object Destructuring

Basic:

const user = { name: "Alice", age: 30, city: "NYC" };

// Old way
const name = user.name;
const age = user.age;

// Destructuring
const { name, age } = user;
console.log(name, age);  // Alice 30

Renaming:

const { name: userName, age: userAge } = user;
console.log(userName, userAge);  // Alice 30

Default values:

const { name, country = "USA" } = user;
console.log(country);  // USA (default, not in object)

Nested destructuring:

const user = {
  name: "Alice",
  address: {
    city: "NYC",
    zip: "10001"
  }
};

const { address: { city, zip } } = user;
console.log(city, zip);  // NYC 10001

Rest in objects:

const { name, ...rest } = user;
console.log(name);  // Alice
console.log(rest);  // { age: 30, city: "NYC" }

Array Destructuring

Basic:

const colors = ["red", "green", "blue"];

const [first, second, third] = colors;
console.log(first);  // red

Skipping elements:

const [, , third] = colors;
console.log(third);  // blue

Rest in arrays:

const [first, ...rest] = colors;
console.log(first);  // red
console.log(rest);   // ["green", "blue"]

Swapping variables:

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

Python comparison:

colors = ["red", "green", "blue"]

first, second, third = colors
first, *rest = colors  # Python unpacking

a, b = 1, 2
a, b = b, a  # Swap

2. Template Literals (Advanced)

Tagged Templates

Functions that process template literals:

function highlight(strings, ...values) {
  return strings.reduce((result, str, i) => {
    return result + str + (values[i] ? `<mark>${values[i]}</mark>` : "");
  }, "");
}

const name = "Alice";
const age = 30;

const result = highlight`Hello ${name}, you are ${age} years old`;
console.log(result);
// Hello <mark>Alice</mark>, you are <mark>30</mark> years old

Use cases:

  • SQL query builders
  • HTML sanitization
  • i18n (internationalization)
  • Styling (styled-components)

Multiline Strings

const message = `
  Hello!
  This is a multi-line string.
  Line breaks are preserved.
`;

// Compare to old way
const oldWay = "Hello!\n" +
               "This is a multi-line string.\n" +
               "Line breaks are preserved.";

3. Optional Chaining (?.)

Safely access nested properties without errors:

Problem:

const user = {
  name: "Alice",
  address: {
    city: "NYC"
  }
};

// This throws error if user.profile doesn't exist
// console.log(user.profile.email);  // Error!

Old solution:

const email = user && user.profile && user.profile.email;

Optional chaining:

const email = user?.profile?.email;
console.log(email);  // undefined (no error!)

// With arrays
const firstPost = user?.posts?.[0];

// With functions
const result = user?.getProfile?.();

Python comparison: Python doesn't have this (would need try/except or multiple checks):

# Python
email = None
if user and hasattr(user, 'profile') and hasattr(user.profile, 'email'):
    email = user.profile.email

4. Nullish Coalescing (??)

Returns right side only if left is null or undefined:

// Old way (||)
const port = config.port || 3000;
// Problem: if port is 0, uses 3000 (0 is falsy)

// Nullish coalescing (??)
const port = config.port ?? 3000;
// Only uses 3000 if port is null/undefined

// Examples
0 ?? 10;         // 0 (0 is not null/undefined)
"" ?? "default"; // "" (empty string is not null/undefined)
false ?? true;   // false
null ?? 10;      // 10
undefined ?? 10; // 10

Combined with optional chaining:

const username = user?.profile?.name ?? "Guest";

5. ES Modules

Export

Named exports:

// utils.js
export function add(a, b) {
  return a + b;
}

export const PI = 3.14159;

export class User {
  constructor(name) {
    this.name = name;
  }
}

// Or export at bottom
function subtract(a, b) {
  return a - b;
}

export { subtract };

Default export:

// calculator.js
export default class Calculator {
  add(a, b) {
    return a + b;
  }
}

// Or
class Calculator { /* ... */ }
export default Calculator;

// Or inline
export default function calculate() { /* ... */ }

Import

Named imports:

import { add, PI } from "./utils.js";

console.log(add(5, 3));  // 8
console.log(PI);         // 3.14159

// Rename
import { add as sum } from "./utils.js";

// Import all
import * as utils from "./utils.js";
console.log(utils.add(5, 3));
console.log(utils.PI);

Default import:

import Calculator from "./calculator.js";

const calc = new Calculator();

Mixed:

import Calculator, { add, PI } from "./utils.js";

Python comparison:

# Python
from utils import add, PI
from utils import add as sum_func
import utils
from calculator import Calculator

Note: ES modules work differently in browser vs Node.js. In browser, need <script type="module">.


6. Modern Array Methods

Array.prototype.at()

Access array elements (supports negative indices):

const arr = [1, 2, 3, 4, 5];

arr.at(0);   // 1
arr.at(-1);  // 5 (last element)
arr.at(-2);  // 4

// Old way for last element
arr[arr.length - 1];  // 5

Python comparison:

arr = [1, 2, 3, 4, 5]
arr[0]    # 1
arr[-1]   # 5 (Python has this built-in!)

Array.prototype.flat()

Flatten nested arrays:

const nested = [1, [2, 3], [4, [5, 6]]];

nested.flat();     // [1, 2, 3, 4, [5, 6]] (depth 1)
nested.flat(2);    // [1, 2, 3, 4, 5, 6] (depth 2)
nested.flat(Infinity);  // Fully flatten

Array.prototype.flatMap()

Map then flatten (one level):

const arr = [1, 2, 3];

// Old way
arr.map(x => [x, x * 2]).flat();  // [1, 2, 2, 4, 3, 6]

// flatMap
arr.flatMap(x => [x, x * 2]);     // [1, 2, 2, 4, 3, 6]

// Practical example
const sentences = ["Hello world", "How are you"];
const words = sentences.flatMap(s => s.split(" "));
console.log(words);  // ["Hello", "world", "How", "are", "you"]

7. Sets

Set = Collection of unique values.

const set = new Set([1, 2, 3, 3, 4, 4, 5]);
console.log(set);  // Set { 1, 2, 3, 4, 5 }

// Methods
set.add(6);
set.has(3);     // true
set.delete(3);
set.size;       // 5
set.clear();    // Remove all

// Iteration
for (const value of set) {
  console.log(value);
}

// Convert to array
const arr = [...set];
const arr2 = Array.from(set);

Use cases:

  • Remove duplicates
  • Check membership (faster than array.includes)
  • Mathematical sets (union, intersection)

Remove duplicates:

const arr = [1, 2, 2, 3, 3, 4];
const unique = [...new Set(arr)];
console.log(unique);  // [1, 2, 3, 4]

Python comparison:

# Python set
s = {1, 2, 3, 3, 4}  # {1, 2, 3, 4}

s.add(5)
3 in s           # True
s.remove(3)
len(s)

# Remove duplicates
arr = [1, 2, 2, 3, 3, 4]
unique = list(set(arr))

8. Maps

Map = Key-value pairs where keys can be any type (not just strings).

const map = new Map();

// Set values
map.set("name", "Alice");
map.set(1, "one");
map.set(true, "boolean key");

// Get values
map.get("name");  // "Alice"
map.get(1);       // "one"

// Check existence
map.has("name");  // true

// Delete
map.delete("name");

// Size
map.size;  // 2

// Iteration
for (const [key, value] of map) {
  console.log(key, value);
}

// Convert to array
const entries = [...map];  // [["name", "Alice"], [1, "one"], ...]

Map vs Object:

FeatureMapObject
Key typesAnyString/Symbol only
Key orderInsertion orderNot guaranteed*
Sizemap.sizeObject.keys(obj).length
Iterationfor...offor...in (needs checks)
PerformanceBetter for frequent add/removeBetter for static data

*Modern JS guarantees order for objects, but Map is explicit

Python comparison:

# Python dict (similar to object)
d = {"name": "Alice", 1: "one"}

d["name"]           # "Alice"
"name" in d         # True
del d["name"]
len(d)

for key, value in d.items():
    print(key, value)

9. Object Enhancements

Shorthand Properties

const name = "Alice";
const age = 30;

// Old
const user = { name: name, age: age };

// Shorthand
const user = { name, age };

Shorthand Methods

// Old
const obj = {
  greet: function() {
    return "Hello";
  }
};

// Shorthand
const obj = {
  greet() {
    return "Hello";
  }
};

Computed Property Names

const key = "dynamicKey";
const obj = {
  [key]: "value",
  [`${key}2`]: "value2",
  [1 + 2]: "three"
};

console.log(obj);
// { dynamicKey: "value", dynamicKey2: "value2", 3: "three" }

Object.entries() & Object.fromEntries()

const obj = { name: "Alice", age: 30 };

// To entries
const entries = Object.entries(obj);
console.log(entries);  // [["name", "Alice"], ["age", 30]]

// From entries
const newObj = Object.fromEntries(entries);
console.log(newObj);  // { name: "Alice", age: 30 }

// Use case: filter object properties
const filtered = Object.fromEntries(
  Object.entries(obj).filter(([key, value]) => value > 25)
);
console.log(filtered);  // { age: 30 }

10. Symbols

Symbol = Unique, immutable identifier.

const sym1 = Symbol("description");
const sym2 = Symbol("description");

console.log(sym1 === sym2);  // false (always unique)

// Use as object keys
const user = {
  name: "Alice",
  [Symbol("id")]: 123
};

// Symbol properties are hidden from Object.keys()
console.log(Object.keys(user));  // ["name"]

// But visible with Object.getOwnPropertySymbols()
console.log(Object.getOwnPropertySymbols(user));

Use cases:

  • Truly private properties (hard to access)
  • Avoid naming conflicts
  • Built-in symbols (Symbol.iterator, etc.)

11. Practical Examples

Example 1: API Response Handler

async function fetchUser(id) {
  const response = await fetch(`https://api.example.com/users/${id}`);
  const data = await response.json();

  // Destructuring with defaults
  const {
    name = "Unknown",
    email,
    profile: { bio = "No bio" } = {}
  } = data;

  return { name, email, bio };
}

Example 2: Data Transformation

const users = [
  { id: 1, name: "Alice", active: true },
  { id: 2, name: "Bob", active: false },
  { id: 3, name: "Charlie", active: true }
];

// Get active user names
const activeNames = users
  .filter(u => u.active)
  .map(({ name }) => name);  // Destructuring in map

console.log(activeNames);  // ["Alice", "Charlie"]

// Convert to Map (id → user)
const userMap = new Map(
  users.map(u => [u.id, u])
);

console.log(userMap.get(1));  // { id: 1, name: "Alice", ... }

Example 3: Configuration with Defaults

function createServer(config = {}) {
  const {
    port = 3000,
    host = "localhost",
    ssl = false,
    ...otherOptions
  } = config;

  console.log(`Server: ${ssl ? "https" : "http"}://${host}:${port}`);
  console.log("Other options:", otherOptions);
}

createServer({ port: 8080, debug: true });
// Server: http://localhost:8080
// Other options: { debug: true }

12. Practical Exercises

Exercise 4.9.1: Destructuring Practice

Rewrite using destructuring:

function displayUser(user) {
  const name = user.name;
  const age = user.age;
  const city = user.address.city;
  console.log(name, age, city);
}

Exercise 4.9.2: Safe Property Access

Rewrite using optional chaining and nullish coalescing:

function getEmail(user) {
  if (user && user.contact && user.contact.email) {
    return user.contact.email;
  }
  return "no-email@example.com";
}

Exercise 4.9.3: Unique Values

Remove duplicates and count occurrences:

const items = ["apple", "banana", "apple", "orange", "banana", "apple"];
// Output: { apple: 3, banana: 2, orange: 1 }

Exercise 4.9.4: Module System

Create a math utilities module with:

  1. Named exports for functions (add, subtract, multiply, divide)
  2. Default export for Calculator class
  3. Import and use in another file

13. Knowledge Check

Question 1: What's the difference between optional chaining (?.) and nullish coalescing (??)?

Show answer Optional chaining (?.) safely accesses nested properties. Nullish coalescing (??) provides default value only for null/undefined (not falsy values like 0 or "").

Question 2: When would you use a Map instead of an Object?

Show answer Use Map when: keys aren't strings, need guaranteed order, frequent add/remove operations, need size property, or iterating key-value pairs.

Question 3: What does Array.flat() do?

Show answer Flattens nested arrays by specified depth. flat(1) removes one level of nesting, flat(Infinity) fully flattens.

Question 4: How do Sets help remove duplicates?

Show answer Sets only store unique values. Create Set from array, then convert back: [...new Set(array)].

Question 5: What's the difference between named and default exports?

Show answer Named exports use exact name in imports. Default export can be imported with any name. Module can have multiple named exports but only one default.

14. Key Takeaways

  • Destructuring extracts values from arrays/objects concisely
  • Template literals support multiline strings and expressions
  • Optional chaining (?.) safely accesses nested properties
  • Nullish coalescing (??) provides defaults for null/undefined only
  • ES modules use import/export for code organization
  • Modern array methods: at() (negative indices), flat() (flatten), flatMap()
  • Sets store unique values, useful for removing duplicates
  • Maps allow any type as keys, better for dynamic key-value data
  • Object shorthand: { name } instead of { name: name }
  • Symbols create unique identifiers

15. Further Resources

Documentation:


Next Steps

Amazing! You now know modern JavaScript features used in production code.

In Lesson 4.10: JavaScript Best Practices & Debugging, you'll learn professional coding practices, debugging techniques, and performance optimization.

Next: Lesson 4.10 - JavaScript Best Practices & Debugging →