Modern JavaScript (ES6+)

Meet Ananya. πŸ‘©β€πŸ’»

She has just joined a small startup in Bengaluru that is building a food delivery app called "DesiEats" πŸ›. On her first day, her team lead Rahul opens an old file from 2012 and sighs:

var self = this;
var name = user && user.profile && user.profile.name ? user.profile.name : "Guest";
var items = order.items.concat([]);
function Restaurant(name) { this.name = name; }
Restaurant.prototype.open = function () { /* ... */ };

"This code works," says Rahul, "but nobody enjoys reading it. Let's rebuild DesiEats with modern JavaScript."

In this lesson, we'll follow Ananya as she rebuilds the app, step by step. Along the way, you'll learn the features that make JavaScript cleaner, safer, and more fun to write. πŸš€

What Does "Modern JavaScript" Mean?

JavaScript is updated every year. A group called TC39 decides which new features get added to the official rulebook, ECMAScript.

The biggest update came in 2015, called ES6 (or ES2015). Since then, a smaller update has arrived every year: ES2016, ES2017, and so on.

πŸ“ Modern JavaScript (often called ES6+) means all the features added from 2015 onwards.

Here's our roadmap for today:

#FeatureWhat it helps with
1let, const, arrow functionsSafer variables, shorter functions
2ES modulesSplitting code into files
3Classes and private fieldsBlueprints for objects
4Constructors and static methodsSetting up objects, class-level helpers
5Inheritance and compositionReusing code
6Destructuring, spread, and restUnpacking and copying data
7Optional chaining and nullish coalescingSafe reading and smart defaults
8Logical assignment operatorsShorter "set if needed" code
9Iterables and iteratorsHow for...of really works
10GeneratorsFunctions that can pause
11Map and SetBetter collections
12Other handy featuresasync/await, at(), structuredClone, and more

Grab your chai β˜•, and let's begin!


Chapter 1: A Quick Warm-Up (let, const, and Arrow Functions)

Before rebuilding anything, Rahul gives Ananya two house rules.

Rule 1: Use const and let, never var

  • const β†’ the variable can't be reassigned
  • let β†’ the variable can change
  • Both live only inside their block { }, which avoids strange bugs that var used to cause

Rule 2: Use arrow functions for short, simple functions

Arrow functions are shorter, and when the body is a single expression, you can skip return and the { }.

⚠️ Remember from the Objects lesson: arrow functions don't have their own this, so don't use them as object methods.

"Great," says Rahul. "Now let's organise the project."


Chapter 2: ES Modules (Splitting Code into Files)

The old DesiEats code lived in one giant file with 5,000 lines. 😱 Finding anything was like searching for your keys in a messy room.

πŸ“ Definition: An ES module is a JavaScript file that can share (export) its code and use (import) code from other files. Each module has its own private scope.

Think of modules like departments in a restaurant 🍽️. The kitchen, the billing counter, and the delivery team each do their own job. They share only what's needed with each other.

πŸ’‘ Module examples need multiple files, so they are shown as normal code blocks, not in the playground.

Named exports: sharing many things

// πŸ“ utils/price.js
export const GST_RATE = 0.05;

export function addGst(amount) {
  return amount + amount * GST_RATE;
}

export function formatPrice(amount) {
  return `β‚Ή${amount.toFixed(2)}`;
}
// πŸ“ app.js
import { addGst, formatPrice } from "./utils/price.js";

console.log(formatPrice(addGst(200)));   // β‚Ή210.00

Key points:

  • The names inside { } must match the exported names.
  • You only import what you need.

Default export: sharing one main thing

A file can have one default export, its "main" item.

// πŸ“ models/Restaurant.js
export default class Restaurant {
  constructor(name) {
    this.name = name;
  }
}
// πŸ“ app.js
import Restaurant from "./models/Restaurant.js";   // no { }, any name works

const r = new Restaurant("Sagar Ratna");

Renaming and importing everything

import { formatPrice as rupees } from "./utils/price.js";   // rename
import * as Price from "./utils/price.js";                  // import everything

console.log(rupees(99));             // β‚Ή99.00
console.log(Price.GST_RATE);         // 0.05

Named vs default: quick comparison

PointNamed exportDefault export
How many per fileManyOnly one
Export syntaxexport const x = ...export default ...
Import syntaximport { x } from "..."import x from "..."
Name when importingMust match (or use as)Any name you like

Using modules in the browser

<script type="module" src="app.js"></script>

The type="module" part is required.

Why modules are great

  • πŸ“‚ Organised: each file has one clear job
  • πŸ”’ Private by default: anything you don't export stays hidden
  • ♻️ Reusable: import the same helper anywhere
  • 🎯 Clear dependencies: you can see exactly what each file uses

πŸ’‘ Fun fact: Modules automatically run in strict mode, which catches common mistakes early.

"Now each part of DesiEats has its own home," smiles Ananya. "Next: the restaurants themselves!"


Chapter 3: Classes and Private Fields

DesiEats has hundreds of restaurants. Each one has a name, a cuisine, a rating, and a menu. Creating each object by hand would be exhausting. 😩

πŸ“ Definition: A class is a blueprint for creating objects. Every object made from a class has the same structure, but its own data.

Think of a class like a cookie cutter πŸͺ. The cutter is the class. Each cookie you cut is an object (also called an instance). Same shape, different flavours!

Your first class

Key points:

  • The class name uses PascalCase: Restaurant, not restaurant.
  • new Restaurant(...) creates a new object from the blueprint.
  • Methods like describe() are shared by every restaurant.

The problem: anyone can change anything 😬

The solution: private fields with #

πŸ“ Definition: A private field starts with #. It can only be used inside the class. Code outside the class cannot read or change it.

Think of it like a bank locker πŸ”. Only the bank's own staff (the class methods) can open it.

Trying to read wallet.#balance from outside the class is a SyntaxError. JavaScript won't even run the code.

Private methods

Methods can be private too:

πŸ’‘ Before # existed, developers used an underscore, like _balance, to mean "please don't touch." But that was only a polite request. # is a real lock.

Public class fields

You can also declare normal (public) fields at the top of a class, without the constructor:


Chapter 4: Constructors and Static Methods

The constructor: setting up a new object

πŸ“ Definition: The constructor is a special method that runs automatically when you create an object with new. It sets up the object's starting values.

Think of it like the welcome desk πŸ›ŽοΈ at a hotel. Every new guest passes through it once, gets a room number, and receives a key.

Key points:

  • A class can have only one constructor.
  • It can have default values, like city = "Bengaluru".
  • It can validate input:

Static methods: belonging to the class itself

Some functions are about the class as a whole, not about one specific object.

πŸ“ Definition: A static method or property belongs to the class itself, not to the objects made from it. You call it on the class name: ClassName.method().

Think of a school 🏫. Each student (object) has their own name and marks. But "total number of students" belongs to the school, not to any one student.

Static factory methods

A popular use of static is a factory: a method that creates objects in a special way.

πŸ’‘ You already use static methods every day: Math.max(), Array.isArray(), Object.keys(), and Number.parseInt() are all static!

Instance methodStatic method
Called onAn object: r1.describe()The class: Restaurant.compare()
Uses this forThat one objectThe class itself
Examplewallet.pay(100)Math.max(1, 2)

Chapter 5: Inheritance and Composition

DesiEats grows. Now there are different kinds of restaurants: cloud kitchens, cafΓ©s, and sweet shops. They share a lot, but each has something special. How can Ananya reuse code without copying it?

There are two main ways: inheritance and composition.

Inheritance: "is a" πŸ‘¨β€πŸ‘©β€πŸ‘§

πŸ“ Definition: Inheritance lets one class (the child) get all the properties and methods of another class (the parent) using the extends keyword.

Think of how children inherit things from their parents, like their surname or eye colour, and still have their own unique qualities.

Key points:

  • extends β†’ "this class is a kind of that class"
  • super(...) β†’ calls the parent's constructor. It must come before using this.
  • super.method() β†’ calls the parent's version of a method
  • Writing a method with the same name in the child is called overriding

⚠️ The problem with too much inheritance

Ananya gets excited and builds a family tree:

Restaurant
 └── DeliveryRestaurant
      └── VegDeliveryRestaurant
           └── VegDeliveryRestaurantWithDineIn
                └── ... 😡

Now a new café wants dine-in and delivery, but it's not veg-only. Where does it fit? Nowhere! The family tree becomes rigid and confusing. This is often called the "gorilla-banana problem": you wanted a banana, but you got the gorilla holding it, and the whole jungle too. 🦍🍌

Composition: "has a" 🧩

πŸ“ Definition: Composition means building an object by combining small, separate abilities, instead of inheriting from a long family tree.

Think of LEGO blocks 🧱. Instead of one fixed toy, you snap together the pieces you need.

Each restaurant picks exactly the abilities it needs. No confusing family tree!

Composition with classes

You can also compose using classes, by giving an object other objects as helpers:

Inheritance vs composition

PointInheritanceComposition
Relationship"is a" (a CloudKitchen is a Restaurant)"has a" (a Checkout has a PaymentService)
KeywordextendsCombining objects
FlexibilityRigid family treeMix and match 🧩
Best forClear, simple, shallow hierarchiesSharing many small abilities

πŸ† Popular advice: "Prefer composition over inheritance." Use inheritance for simple, clear "is a" relationships, and composition when you need flexibility.


Chapter 6: Destructuring, Spread, and Rest

Every order that arrives from the DesiEats server is a big object. Ananya is tired of writing order.customer.name, order.customer.phone, order.total... again and again. 😫

Object destructuring

πŸ“ Definition: Destructuring lets you unpack values from objects or arrays into separate variables, in one line.

Think of opening a tiffin box 🍱 and putting each item on its own plate.

Array destructuring

Destructuring in function parameters ⭐

This is how most modern code is written:

Spread ...: spreading things out

Spread expands an array or object into its individual pieces.

Rest ...: collecting things together

Rest looks the same (...), but it collects many values into one.

πŸ’‘ Easy trick: On the right side of = or in a function call, ... spreads things out. On the left side of = or in function parameters, ... collects them.


Chapter 7: Optional Chaining and Nullish Coalescing

One night, the DesiEats app crashes. πŸ’₯ Ananya checks the error:

TypeError: Cannot read properties of undefined (reading 'city')

A new customer hadn't added an address yet, and the code tried to read user.address.city.

Optional chaining ?.

πŸ“ Definition: Optional chaining ?. lets you safely read a value deep inside an object. If any part along the way is null or undefined, it stops and returns undefined instead of crashing.

Think of a security guard πŸ’‚ at each gate. If a gate is closed, you calmly turn back instead of crashing into it.

SyntaxUse for
obj?.propA property
obj?.[key]Bracket access or an array item
obj.method?.()A method that might not exist

Nullish coalescing ??

Now Ananya wants to show "City not added" when there's no city. She first tries ||:

The problem: || treats every falsy value (0, "", false) as "missing."

πŸ“ Definition: Nullish coalescing ?? gives a default value only when the left side is null or undefined. Real values like 0, "", and false are kept.

|| vs ??

Value on the leftvalue || "default"value ?? "default"
null"default""default"
undefined"default""default"
0"default" ⚠️0 βœ…
"""default" ⚠️"" βœ…
false"default" ⚠️false βœ…

πŸ† Power combo: ?. and ?? work beautifully together: user.address?.city ?? "Not added".


Chapter 8: Logical Assignment Operators

Ananya notices she keeps writing lines like these:

if (!settings.theme) settings.theme = "light";
if (cart.items === null || cart.items === undefined) cart.items = [];
if (user.isPremium) user.discount = 20;

Modern JavaScript has shortcuts for exactly this, called logical assignment operators.

πŸ“ Definition: Logical assignment operators (||=, &&=, ??=) combine a logical check and an assignment in one step. They only assign when the condition is met.

OperatorMeaningSame as
a ||= bAssign if a is falsya || (a = b)
a &&= bAssign if a is truthya && (a = b)
a ??= bAssign if a is null or undefineda ?? (a = b)

??=: fill in only what's missing

||=: replace empty or falsy values

&&=: update only if it already exists

Real example: counting orders per restaurant

πŸ’‘ Which one should I use? When in doubt, pick ??=. It's the safest, because it never overwrites real values like 0 or false.


Chapter 9: Iterables and Iterators

Ananya loves for...of. It works on arrays, strings, Maps, and Sets. But one day she tries it on her own Menu object:

"Why does for...of work on arrays, but not on my object?" she wonders. Let's find out! πŸ•΅οΈβ€β™€οΈ

Two important words

πŸ“ Iterable: An object that can be looped over with for...of. It has a special method called [Symbol.iterator].

πŸ“ Iterator: An object that hands out values one at a time, through a next() method.

Think of a token machine 🎟️ at a bank:

  • The bank is the iterable. You can ask it for a token machine.
  • The token machine is the iterator. Every time you press the button (next()), it gives you the next token.
  • When the tokens run out, it says "done."

Seeing an iterator in action

Every array secretly has a [Symbol.iterator] method. Let's call it by hand:

Each call to next() returns an object with two properties:

  • value β†’ the current item
  • done β†’ true when there's nothing left

This is exactly what for...of does behind the scenes! It keeps calling next() until done is true.

Making your own iterable

To make Menu work with for...of, give it a [Symbol.iterator] method that returns an iterator:

Now for...of, spread ..., and array destructuring all work with menu. πŸŽ‰

What uses iterables?

FeatureExample
for...offor (const x of iterable)
Spread[...iterable]
Array destructuringconst [a, b] = iterable
Array.from()Array.from(iterable)
new Set() / new Map()new Set(iterable)

Built-in iterables: arrays, strings, Maps, Sets, arguments, and many browser lists. Plain objects are not iterable.

πŸ˜… "Writing next() by hand is a lot of work," Ananya admits. Rahul smiles: "That's why we have generators."


Chapter 10: Generators

πŸ“ Definition: A generator is a special function that can pause and resume. It is written with function*, and it uses yield to give out values one at a time.

Think of a TV series πŸ“Ί on a streaming app. You watch one episode, then pause. When you come back, it continues from where you stopped, not from the beginning. A generator works the same way.

Your first generator

How it works:

  1. Calling orderStatus() doesn't run the code. It returns a generator object (which is an iterator).
  2. Each next() runs the code until the next yield, then pauses.
  3. When there are no more yields, done becomes true.

Generators work with for...of

Generators are iterable, so you can loop over them directly:

The easy way to make an iterable ✨

Remember the long Menu iterator from Chapter 9? Here's the same thing with a generator:

Just a few lines instead of a whole next() method. πŸ™Œ

Generators with classes

Infinite generators

Because a generator pauses, it can produce values forever without freezing your computer. It only makes the next value when you ask for it.

⚠️ Never use for...of or spread ... on an infinite generator. They would try to read forever! Use next() or add a break.

Taking only what you need

Sending values into a generator

next(value) can also send a value back into the generator. The value becomes the result of the paused yield.

When are generators useful?

  • πŸ” Making objects iterable easily
  • ♾️ Creating endless sequences, like IDs or pages of results
  • 🐒 Lazy work: calculating values only when they are needed
  • 🧩 Step-by-step processes, like order tracking or form wizards

Chapter 11: Map and Set (Better Collections)

As DesiEats grows, Ananya needs better tools to store data.

Set: a collection of unique values

πŸ“ Definition: A Set stores unique values. Duplicates are ignored automatically.

Map: key-value pairs with superpowers

πŸ“ Definition: A Map stores key-value pairs like an object, but any value (even an object) can be a key, it remembers the insertion order, and it has a handy .size.

NeedUse
A list of unique valuesSet
Key-value pairs with any type of key, or frequent adding/removingMap
Simple, fixed data like a user profileA normal object { }

Chapter 12: Other Handy Modern Features

Here's a quick tour of more modern features that Ananya uses every day. πŸŽ’

async / await: waiting without freezing

Fetching data from a server takes time. async/await lets you write "waiting" code that reads top to bottom, like normal code.

You'll learn Promises and async/await in detail in the Async JavaScript lesson.

Array.prototype.at(): easy negative indexes

structuredClone(): true deep copy

Object.hasOwn(): safe key checking

Non-mutating array methods

Numeric separators

Object.groupBy(): grouping made easy

⚠️ Some very new features, like Object.groupBy() and toSorted(), need an up-to-date browser.


The Happy Ending πŸŽ‰

Three months later, Rahul opens the new DesiEats code. Instead of the messy old file, he sees this:

Classes, private fields, static members, destructuring, default values, ??=, ?., ??, and generators, all working together in a few clean lines. 😍

"This," says Rahul, "is modern JavaScript."

Ananya smiles and orders a celebratory masala dosa. On DesiEats, of course. πŸ›


Common Mistakes to Avoid

MistakeProblemFix
Forgetting type="module" in HTMLimport/export don't work<script type="module">
Using this before super() in a child class❌ ReferenceErrorCall super(...) first
Reading #private from outside the class❌ SyntaxErrorAdd a public method or getter
Calling a static method on an objectundefined / TypeErrorCall it on the class: ClassName.method()
Using || for defaults when 0 or "" are validReal values get replacedUse ??
Using for...of on a plain object❌ TypeError: not iterableAdd [Symbol.iterator] or use Object.entries()
Spreading an infinite generatorRuns foreverUse next() or break
Deep inheritance treesRigid, confusing codePrefer composition

Quick Summary Table

FeatureExample
Named export / importexport const x = 1; β†’ import { x } from "./a.js";
Default export / importexport default App; β†’ import App from "./App.js";
Classclass Cart { constructor() { } }
Private field#total = 0;
Static methodstatic create() { } β†’ Cart.create()
Inheritanceclass Cafe extends Restaurant { }
Composition{ ...canDeliver(state), ...canCook(state) }
Destructuringconst { name, city = "Pune" } = user;
Spread / rest[...a, ...b] / function f(...args) { }
Optional chaininguser.address?.city
Nullish coalescingvalue ?? "default"
Logical assignmenta ??= b, a ||= b, a &&= b
Iterable[Symbol.iterator]() { return { next() { } }; }
Generatorfunction* gen() { yield 1; }

Key Takeaways

  • Modern JavaScript (ES6+) adds features from 2015 onwards that make code cleaner, safer, and easier to read.
  • ES modules split code into files with export and import. Classes are blueprints for objects, and #private fields keep data truly safe.
  • Constructors set up new objects, and static members belong to the class itself. Use inheritance for simple "is a" relationships, and prefer composition for flexible "has a" designs.
  • Destructuring, spread/rest, ?., ??, and ??=/||=/&&= make working with data short and safe.
  • Iterables power for...of, and generators (function* + yield) are the easiest way to create them, including endless, lazy sequences.