ES6+ Features

Classes and Modules

Building Blocks of Modern JavaScript

Imagine being a builder with only basic tools and no blueprint system - that was JavaScript before ES6! Today, we're exploring two revolutionary features that transformed how we structure code: Classes and Modules. These features moved JavaScript from a script-based language to a truly object-oriented, modular programming language capable of handling enterprise-scale applications.

Why Classes and Modules Matter

graph TD A[Pre-ES6 JavaScript] --> B[Prototype-based Inheritance] A --> C[Global Namespace Pollution] A --> D[Script-based Loading] A --> E[Manual Dependency Management] F[ES6+ JavaScript] --> G[Class-based Syntax] F --> H[Private Scope with Modules] F --> I[Import/Export System] F --> J[Automated Dependency Resolution] style A fill:#f96,stroke:#333 style F fill:#6f9,stroke:#333

Classes provide a clear, familiar syntax for creating objects and implementing inheritance, while modules help us organize code into reusable, maintainable, and encapsulated units.

Classes: Syntactic Sugar for Prototypes

JavaScript classes are primarily syntactic sugar over the existing prototype-based inheritance. They provide a cleaner, more intuitive way to create constructor functions and manage inheritance.

From Constructor Functions to Classes

// Pre-ES6 constructor function approach
function Person(name, age) {
  this.name = name;
  this.age = age;
}

Person.prototype.greet = function() {
  return `Hello, my name is ${this.name} and I am ${this.age} years old.`;
};

// Create a new instance
const alice = new Person('Alice', 28);
console.log(alice.greet()); // Hello, my name is Alice and I am 28 years old.

// ES6 class approach
class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  
  greet() {
    return `Hello, my name is ${this.name} and I am ${this.age} years old.`;
  }
}

// Create a new instance (same as before!)
const bob = new Person('Bob', 32);
console.log(bob.greet()); // Hello, my name is Bob and I am 32 years old.

Class Structure Visualization

classDiagram class Person { +String name +Number age +constructor(name, age) +greet() }

Class Features and Inheritance

Class Properties and Methods

class User {
  // Class properties (not in original ES6, but widely supported now)
  role = 'user';
  loginCount = 0;
  
  // Constructor - called when a new instance is created
  constructor(username, email) {
    this.username = username;
    this.email = email;
    this.createdAt = new Date();
  }
  
  // Instance method - accessible by instances
  login() {
    this.loginCount++;
    return `${this.username} logged in`;
  }
  
  // Static method - accessible on the class itself, not instances
  static compareUsers(userA, userB) {
    return userA.loginCount - userB.loginCount;
  }
  
  // Getters and setters for computed properties
  get formattedCreationDate() {
    return this.createdAt.toLocaleDateString();
  }
  
  set email(value) {
    if (!value.includes('@')) {
      throw new Error('Invalid email format');
    }
    this._email = value.toLowerCase();
  }
  
  get email() {
    return this._email;
  }
}

// Using the class
const user1 = new User('alice', 'alice@example.com');
console.log(user1.login());             // alice logged in
console.log(user1.formattedCreationDate); // Gets current date in local format

// Using static method
const user2 = new User('bob', 'bob@example.com');
user2.login();
user2.login();
console.log(User.compareUsers(user2, user1)); // 1 (bob has 1 more login)

// Setter validation
try {
  const user3 = new User('charlie', 'invalid-email');
} catch (error) {
  console.error(error.message); // Invalid email format
}

Class Inheritance

// Base class
class Vehicle {
  constructor(make, model, year) {
    this.make = make;
    this.model = model;
    this.year = year;
    this.isRunning = false;
  }
  
  start() {
    this.isRunning = true;
    return `${this.make} ${this.model} started`;
  }
  
  stop() {
    this.isRunning = false;
    return `${this.make} ${this.model} stopped`;
  }
  
  toString() {
    return `${this.year} ${this.make} ${this.model}`;
  }
}

// Derived class (inherits from Vehicle)
class Car extends Vehicle {
  constructor(make, model, year, numDoors) {
    // Must call super() before accessing 'this'
    super(make, model, year);
    this.numDoors = numDoors;
    this.type = 'car';
  }
  
  // Override parent method
  toString() {
    return `${super.toString()}, ${this.numDoors}-door`;
  }
  
  // New method specific to Car
  honk() {
    return 'Beep beep!';
  }
}

// Another derived class
class Motorcycle extends Vehicle {
  constructor(make, model, year) {
    super(make, model, year);
    this.type = 'motorcycle';
  }
  
  wheelie() {
    return `Performing a wheelie on ${this.make} ${this.model}!`;
  }
}

// Using inheritance
const car = new Car('Toyota', 'Camry', 2022, 4);
console.log(car.toString());  // 2022 Toyota Camry, 4-door
console.log(car.start());     // Toyota Camry started
console.log(car.honk());      // Beep beep!

const bike = new Motorcycle('Harley', 'Street Glide', 2021);
console.log(bike.toString()); // 2021 Harley Street Glide
console.log(bike.wheelie());  // Performing a wheelie on Harley Street Glide!

Class Inheritance Visualization

classDiagram Vehicle <|-- Car Vehicle <|-- Motorcycle Vehicle : +String make Vehicle : +String model Vehicle : +Number year Vehicle : +Boolean isRunning Vehicle : +start() Vehicle : +stop() Vehicle : +toString() Car : +Number numDoors Car : +String type Car : +toString() Car : +honk() Motorcycle : +String type Motorcycle : +wheelie()

Private Class Features

ES2022 introduced private fields and methods using the # prefix, providing true encapsulation in JavaScript classes.

class BankAccount {
  // Private fields (only accessible within the class)
  #balance = 0;
  #transactions = [];
  #accountNumber;
  
  constructor(owner, initialDeposit = 0) {
    this.owner = owner;
    this.#accountNumber = this.#generateAccountNumber();
    
    if (initialDeposit > 0) {
      this.deposit(initialDeposit);
    }
  }
  
  // Public methods
  deposit(amount) {
    if (amount <= 0) {
      throw new Error('Deposit amount must be positive');
    }
    
    this.#balance += amount;
    this.#addTransaction('deposit', amount);
    return this.#balance;
  }
  
  withdraw(amount) {
    if (amount <= 0) {
      throw new Error('Withdrawal amount must be positive');
    }
    
    if (amount > this.#balance) {
      throw new Error('Insufficient funds');
    }
    
    this.#balance -= amount;
    this.#addTransaction('withdrawal', amount);
    return this.#balance;
  }
  
  getBalance() {
    return this.#balance;
  }
  
  getAccountDetails() {
    return {
      owner: this.owner,
      accountNumber: this.#accountNumber,
      balance: this.#balance,
      transactionCount: this.#transactions.length
    };
  }
  
  // Private methods
  #addTransaction(type, amount) {
    this.#transactions.push({
      type,
      amount,
      date: new Date()
    });
  }
  
  #generateAccountNumber() {
    return Math.floor(Math.random() * 1000000000).toString().padStart(9, '0');
  }
}

// Using the BankAccount class
const account = new BankAccount('Alice', 1000);
console.log(account.getBalance()); // 1000

account.deposit(500);
console.log(account.getBalance()); // 1500

account.withdraw(200);
console.log(account.getBalance()); // 1300

console.log(account.getAccountDetails());
// { owner: 'Alice', accountNumber: '123456789', balance: 1300, transactionCount: 3 }

// Private members are not accessible outside the class
// console.log(account.#balance); // SyntaxError
// account.#addTransaction('hack', 1000000); // SyntaxError

Class Comparison with Other Languages

JavaScript's class syntax makes it more approachable for developers coming from other languages, though there are important differences.

// JavaScript classes - fundamentally prototype-based
class JavaScriptPerson {
  constructor(name) {
    this.name = name;
  }
  
  sayHello() {
    return `Hello, I'm ${this.name}`;
  }
}

// In Java or C#, classes would look something like:
/*
public class JavaPerson {
  private String name;
  
  public JavaPerson(String name) {
    this.name = name;
  }
  
  public String sayHello() {
    return "Hello, I'm " + this.name;
  }
}
*/

// Key differences:
// 1. JavaScript has no access modifiers (public, private, protected) in the standard syntax
// 2. JavaScript uses prototype-based inheritance under the hood
// 3. JavaScript classes are first-class citizens (can be passed as values)
// 4. JavaScript doesn't support method overloading

// Passing a class as a value (first-class citizen)
function createInstance(ClassDefinition, ...args) {
  return new ClassDefinition(...args);
}

const john = createInstance(JavaScriptPerson, 'John');
console.log(john.sayHello()); // Hello, I'm John

Real-World Class Examples

Building a Shopping Cart

class Product {
  constructor(id, name, price) {
    this.id = id;
    this.name = name;
    this.price = price;
  }
}

class CartItem {
  constructor(product, quantity = 1) {
    this.product = product;
    this.quantity = quantity;
  }
  
  get total() {
    return this.product.price * this.quantity;
  }
}

class ShoppingCart {
  #items = [];
  
  addItem(product, quantity = 1) {
    const existingItem = this.#items.find(item => item.product.id === product.id);
    
    if (existingItem) {
      existingItem.quantity += quantity;
    } else {
      this.#items.push(new CartItem(product, quantity));
    }
  }
  
  removeItem(productId) {
    const index = this.#items.findIndex(item => item.product.id === productId);
    if (index !== -1) {
      this.#items.splice(index, 1);
    }
  }
  
  updateQuantity(productId, quantity) {
    const item = this.#items.find(item => item.product.id === productId);
    if (item) {
      if (quantity > 0) {
        item.quantity = quantity;
      } else {
        this.removeItem(productId);
      }
    }
  }
  
  get items() {
    return [...this.#items];
  }
  
  get totalItems() {
    return this.#items.reduce((total, item) => total + item.quantity, 0);
  }
  
  get totalPrice() {
    return this.#items.reduce((total, item) => total + item.total, 0);
  }
  
  clearCart() {
    this.#items = [];
  }
}

// Usage example
const laptop = new Product(1, 'Laptop', 999.99);
const phone = new Product(2, 'Smartphone', 699.99);
const headphones = new Product(3, 'Headphones', 149.99);

const cart = new ShoppingCart();
cart.addItem(laptop);
cart.addItem(phone, 2);
cart.addItem(headphones);

console.log(`Items in cart: ${cart.totalItems}`);
console.log(`Total price: $${cart.totalPrice.toFixed(2)}`);

cart.updateQuantity(1, 2); // Update laptop quantity
cart.removeItem(3); // Remove headphones

console.log(`Updated items in cart: ${cart.totalItems}`);
console.log(`Updated total price: $${cart.totalPrice.toFixed(2)}`);

// Output cart items
cart.items.forEach(item => {
  console.log(`${item.product.name} x${item.quantity}: $${item.total.toFixed(2)}`);
});

Form Validator Class

class FormValidator {
  #form;
  #validations = {};
  #errors = {};
  
  constructor(formElement) {
    this.#form = formElement;
    this.#setupEventListeners();
  }
  
  addValidation(fieldName, validationFn, errorMessage) {
    if (!this.#validations[fieldName]) {
      this.#validations[fieldName] = [];
    }
    
    this.#validations[fieldName].push({
      validate: validationFn,
      message: errorMessage
    });
    
    return this; // For method chaining
  }
  
  #setupEventListeners() {
    this.#form.addEventListener('submit', (event) => {
      if (!this.validateAll()) {
        event.preventDefault();
        this.#showErrors();
      }
    });
    
    // Real-time validation as user types
    this.#form.addEventListener('input', (event) => {
      const field = event.target;
      this.validateField(field.name);
      this.#updateFieldError(field.name);
    });
  }
  
  validateField(fieldName) {
    const field = this.#form.elements[fieldName];
    
    if (!field || !this.#validations[fieldName]) {
      return true;
    }
    
    this.#errors[fieldName] = [];
    
    const validations = this.#validations[fieldName];
    let isValid = true;
    
    for (const validation of validations) {
      if (!validation.validate(field.value)) {
        this.#errors[fieldName].push(validation.message);
        isValid = false;
      }
    }
    
    return isValid;
  }
  
  validateAll() {
    let isValid = true;
    
    for (const fieldName in this.#validations) {
      if (!this.validateField(fieldName)) {
        isValid = false;
      }
    }
    
    return isValid;
  }
  
  #showErrors() {
    for (const fieldName in this.#errors) {
      this.#updateFieldError(fieldName);
    }
  }
  
  #updateFieldError(fieldName) {
    const field = this.#form.elements[fieldName];
    const errorContainer = this.#form.querySelector(`[data-error-for="${fieldName}"]`);
    
    if (!errorContainer) return;
    
    const errors = this.#errors[fieldName] || [];
    
    if (errors.length > 0) {
      errorContainer.textContent = errors[0];
      errorContainer.style.display = 'block';
      field.classList.add('invalid');
    } else {
      errorContainer.textContent = '';
      errorContainer.style.display = 'none';
      field.classList.remove('invalid');
    }
  }
  
  getErrors() {
    return { ...this.#errors };
  }
}

// Usage example
document.addEventListener('DOMContentLoaded', () => {
  const form = document.getElementById('registrationForm');
  
  const validator = new FormValidator(form)
    .addValidation('username', value => value.length >= 3, 
      'Username must be at least 3 characters')
    .addValidation('email', value => /^.+@.+\..+$/.test(value), 
      'Please enter a valid email address')
    .addValidation('password', value => value.length >= 8, 
      'Password must be at least 8 characters')
    .addValidation('password', value => /[A-Z]/.test(value), 
      'Password must include at least one uppercase letter')
    .addValidation('confirmPassword', value => value === form.elements.password.value, 
      'Passwords do not match');
});

Modules: Organizing JavaScript Code

ES6 modules provide a standardized way to organize and reuse JavaScript code across multiple files.

graph TD A[Before Modules] --> B["Global Scope (window/global objects)"] A --> C["Script Dependencies (Manual Ordering)"] A --> D["Script Tags (Multiple HTTP Requests)"] E[With Modules] --> F["Module Scope (Private by Default)"] E --> G["Explicit Imports/Exports (Clear Dependencies)"] E --> H["Tree-shaking (Dead Code Elimination)"] style A fill:#f96,stroke:#333 style E fill:#6f9,stroke:#333

Module Syntax

Export Syntax

// math.js - Different ways to export

// Named exports (can have multiple)
export function add(a, b) {
  return a + b;
}

export function subtract(a, b) {
  return a - b;
}

// Export variable
export const PI = 3.14159;

// Export after declaration
function multiply(a, b) {
  return a * b;
}
function divide(a, b) {
  if (b === 0) throw new Error('Cannot divide by zero');
  return a / b;
}
const EULER = 2.71828;

// Group exports
export { multiply, divide, EULER };

// Export with rename
export { divide as safelyDivide };

// Default export (only one per module)
export default function calculateArea(radius) {
  return PI * radius * radius;
}

Import Syntax

// app.js - Different ways to import

// Import named exports
import { add, subtract, PI } from './math.js';
console.log(add(2, 3));        // 5
console.log(subtract(10, 4));  // 6
console.log(PI);               // 3.14159

// Import with rename
import { add as sum } from './math.js';
console.log(sum(5, 5));        // 10

// Import default export
import calculateArea from './math.js';
console.log(calculateArea(5)); // 78.53975

// Import default and named exports
import calculateArea, { multiply, divide } from './math.js';
console.log(multiply(3, 4));   // 12

// Import all exports as a namespace object
import * as MathUtils from './math.js';
console.log(MathUtils.add(1, 2));       // 3
console.log(MathUtils.PI);              // 3.14159
console.log(MathUtils.default(3));      // 28.27431

Module Organization

Project Structure

project/
├── src/
│   ├── index.js           # Main entry point
│   ├── utils/
│   │   ├── math.js        # Math utilities
│   │   ├── validation.js  # Validation helpers
│   │   └── formatting.js  # Formatting functions
│   ├── components/
│   │   ├── Button.js      # Button component
│   │   ├── Form.js        # Form component
│   │   └── Modal.js       # Modal component
│   └── services/
│       ├── api.js         # API service
│       └── auth.js        # Authentication service
├── index.html
└── package.json

Barrel Files (Re-exporting)

// src/utils/index.js - Barrel file for utilities
// Re-export functionality from multiple files
export * from './math.js';
export * from './validation.js';
export * from './formatting.js';

// src/components/index.js - Barrel file for components
export { default as Button } from './Button.js';
export { default as Form } from './Form.js';
export { default as Modal } from './Modal.js';

// Then in your app, you can import more easily:
import { add, subtract, formatCurrency } from './utils';
import { Button, Form } from './components';

Module Dependency Visualization

graph TD A[index.js] --> B[utils/index.js] A --> C[components/index.js] A --> D[services/api.js] B --> E[utils/math.js] B --> F[utils/validation.js] B --> G[utils/formatting.js] C --> H[components/Button.js] C --> I[components/Form.js] C --> J[components/Modal.js] D --> K[services/auth.js] I --> F

Dynamic Import

Dynamic imports allow you to load modules on-demand, which can improve performance by reducing initial load time.

// Static import (loads on script start)
import { add } from './math.js';

// Dynamic import (loads when needed)
async function calculateWithDynamicImport() {
  try {
    // Math module loaded only when the function is called
    const mathModule = await import('./math.js');
    
    // Use the dynamically imported functions
    const result = mathModule.add(5, 10);
    console.log(result); // 15
    
    // Use default export with .default
    const area = mathModule.default(7);
    console.log(area); // 153.93791
    
  } catch (error) {
    console.error('Failed to load module:', error);
  }
}

// Using dynamic import for conditional loading
function handleUserAction(actionType) {
  if (actionType === 'process-data') {
    import('./data-processor.js')
      .then(module => {
        module.processData();
      })
      .catch(err => console.error(err));
  }
  else if (actionType === 'generate-report') {
    import('./report-generator.js')
      .then(module => {
        module.generateReport();
      })
      .catch(err => console.error(err));
  }
}

// Code splitting with dynamic import
async function loadPageModule(pageName) {
  try {
    // Only load the necessary module for the current page
    const pageModule = await import(`./pages/${pageName}.js`);
    return pageModule.default;
  } catch (error) {
    console.error(`Failed to load ${pageName} module:`, error);
    return null;
  }
}

// Load module based on the current page
document.addEventListener('DOMContentLoaded', () => {
  const currentPage = document.body.dataset.page;
  loadPageModule(currentPage).then(pageModule => {
    if (pageModule) pageModule.init();
  });
});

Real-World Module Examples

Simple Class and Module Integration

// src/models/User.js
export class User {
  constructor(id, name, email) {
    this.id = id;
    this.name = name;
    this.email = email;
  }
  
  getDisplayName() {
    return this.name || this.email.split('@')[0];
  }
  
  toJSON() {
    return {
      id: this.id,
      name: this.name,
      email: this.email
    };
  }
}

export class Admin extends User {
  constructor(id, name, email, permissions = []) {
    super(id, name, email);
    this.permissions = permissions;
    this.role = 'admin';
  }
  
  hasPermission(permission) {
    return this.permissions.includes(permission);
  }
  
  toJSON() {
    return {
      ...super.toJSON(),
      role: this.role,
      permissions: this.permissions
    };
  }
}

// Export a factory function as default export
export default function createUser(userData) {
  if (userData.role === 'admin') {
    return new Admin(
      userData.id,
      userData.name,
      userData.email,
      userData.permissions
    );
  }
  
  return new User(userData.id, userData.name, userData.email);
}

// src/services/userService.js
import createUser, { User, Admin } from '../models/User.js';

const users = [];

export function addUser(userData) {
  const user = createUser(userData);
  users.push(user);
  return user;
}

export function getUserById(id) {
  return users.find(user => user.id === id) || null;
}

export function getAllUsers() {
  return [...users];
}

// src/index.js
import { addUser, getUserById, getAllUsers } from './services/userService.js';

// Add some users
addUser({ id: 1, name: 'John Doe', email: 'john@example.com' });
addUser({ id: 2, name: 'Jane Smith', email: 'jane@example.com' });
addUser({ 
  id: 3, 
  name: 'Admin User', 
  email: 'admin@example.com',
  role: 'admin',
  permissions: ['create', 'read', 'update', 'delete']
});

// Use the service
console.log(getAllUsers());
const user = getUserById(3);
console.log(user.getDisplayName());
if (user.hasPermission && user.hasPermission('delete')) {
  console.log('Admin has delete permission');
}

API Service with Modules

// src/config.js
export const API_BASE_URL = 'https://api.example.com/v1';
export const API_TIMEOUT = 5000;

// src/utils/http.js
import { API_BASE_URL, API_TIMEOUT } from '../config.js';

export class RequestError extends Error {
  constructor(message, status, data = null) {
    super(message);
    this.name = 'RequestError';
    this.status = status;
    this.data = data;
  }
}

export default async function request(url, options = {}) {
  const requestUrl = url.startsWith('http')
    ? url
    : `${API_BASE_URL}${url}`;
    
  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), API_TIMEOUT);
  
  try {
    const response = await fetch(requestUrl, {
      ...options,
      headers: {
        'Content-Type': 'application/json',
        ...options.headers
      },
      signal: controller.signal
    });
    
    clearTimeout(timeoutId);
    
    const data = await response.json();
    
    if (!response.ok) {
      throw new RequestError(
        data.message || 'Request failed',
        response.status,
        data
      );
    }
    
    return data;
  } catch (error) {
    clearTimeout(timeoutId);
    
    if (error.name === 'AbortError') {
      throw new RequestError('Request timeout', 408);
    }
    
    throw error;
  }
}

// src/services/postsService.js
import request from '../utils/http.js';

export async function getPosts(page = 1, limit = 10) {
  return request(`/posts?page=${page}&limit=${limit}`);
}

export async function getPostById(id) {
  return request(`/posts/${id}`);
}

export async function createPost(postData) {
  return request('/posts', {
    method: 'POST',
    body: JSON.stringify(postData)
  });
}

export async function updatePost(id, postData) {
  return request(`/posts/${id}`, {
    method: 'PUT',
    body: JSON.stringify(postData)
  });
}

export async function deletePost(id) {
  return request(`/posts/${id}`, {
    method: 'DELETE'
  });
}

// src/main.js - Usage
import { getPosts, createPost } from './services/postsService.js';

async function loadPosts() {
  try {
    const posts = await getPosts(1, 5);
    console.log('Posts loaded:', posts);
    
    // Use the posts data...
    displayPosts(posts);
  } catch (error) {
    console.error('Failed to load posts:', error.message);
    showErrorMessage(error);
  }
}

async function submitPost(title, content) {
  try {
    const newPost = await createPost({ title, content });
    console.log('Post created:', newPost);
    
    // Update UI with new post...
    addPostToList(newPost);
  } catch (error) {
    console.error('Failed to create post:', error.message);
    showErrorMessage(error);
  }
}

// Initializing the application
document.addEventListener('DOMContentLoaded', () => {
  loadPosts();
  
  const form = document.getElementById('postForm');
  form.addEventListener('submit', event => {
    event.preventDefault();
    const title = form.elements.title.value;
    const content = form.elements.content.value;
    submitPost(title, content);
  });
});

Module Best Practices

Organization and Naming

// ✅ DO: One responsibility per module
// userAuthentication.js - Only handles authentication
export function login(username, password) { /* ... */ }
export function logout() { /* ... */ }
export function validateToken(token) { /* ... */ }

// ❌ DON'T: Mix unrelated functionality
// utils.js - Too many unrelated functions
export function formatDate(date) { /* ... */ }
export function calculateTax(amount) { /* ... */ }
export function validateEmail(email) { /* ... */ }
export function fetchData(url) { /* ... */ }

// ✅ DO: Use consistent naming conventions
// kebab-case for files
// camelCase for functions and variables
// PascalCase for classes

// ✅ DO: Group related modules
// services/auth.js
// services/api.js
// services/storage.js

// ✅ DO: Use index.js barrel files to simplify imports
// src/utils/index.js
export * from './date-utils.js';
export * from './validation-utils.js';
export * from './formatting-utils.js';

Import and Export Best Practices

// ✅ DO: Be explicit about imports
import { useState, useEffect } from 'react';

// ❌ DON'T: Import everything unless necessary
import * as React from 'react'; // Imports everything, could be overkill

// ✅ DO: Use default exports for the main thing a module provides
// Button.js
export default function Button(props) { /* ... */ }

// ✅ DO: Use named exports for utility functions and multiple exports
// date-utils.js
export function formatDate(date) { /* ... */ }
export function parseDate(dateString) { /* ... */ }

// ✅ DO: Prefer constant references
// constants.js
export const API_URL = 'https://api.example.com';
export const MAX_ITEMS = 100;

// ❌ DON'T: Export mutable variables
// config.js
export let apiUrl = 'https://api.example.com'; // Can change unexpectedly

// ✅ DO: Re-export external dependencies to centralize version control
// src/lib/index.js
export { default as axios } from 'axios';
export { v4 as uuidv4 } from 'uuid';

Using Classes and Modules Together

// src/models/Task.js
export class Task {
  constructor(id, title, status = 'pending') {
    this.id = id;
    this.title = title;
    this.status = status;
    this.createdAt = new Date();
    this.updatedAt = new Date();
  }
  
  complete() {
    this.status = 'completed';
    this.updatedAt = new Date();
  }
  
  toJSON() {
    return {
      id: this.id,
      title: this.title,
      status: this.status,
      createdAt: this.createdAt,
      updatedAt: this.updatedAt
    };
  }
}

// src/services/TaskService.js
import { Task } from '../models/Task.js';

class TaskService {
  #tasks = [];
  #nextId = 1;
  
  createTask(title) {
    const task = new Task(this.#nextId++, title);
    this.#tasks.push(task);
    return task;
  }
  
  getTaskById(id) {
    return this.#tasks.find(task => task.id === id) || null;
  }
  
  getAllTasks() {
    return [...this.#tasks];
  }
  
  updateTask(id, updates) {
    const task = this.getTaskById(id);
    if (!task) return null;
    
    Object.assign(task, { ...updates, updatedAt: new Date() });
    return task;
  }
  
  completeTask(id) {
    const task = this.getTaskById(id);
    if (!task) return false;
    
    task.complete();
    return true;
  }
  
  deleteTask(id) {
    const index = this.#tasks.findIndex(task => task.id === id);
    if (index === -1) return false;
    
    this.#tasks.splice(index, 1);
    return true;
  }
}

// Export a singleton instance
export default new TaskService();

// src/ui/TaskUI.js
import taskService from '../services/TaskService.js';

export function initTaskUI() {
  const taskList = document.getElementById('taskList');
  const taskForm = document.getElementById('taskForm');
  
  function renderTasks() {
    const tasks = taskService.getAllTasks();
    
    taskList.innerHTML = tasks.map(task => `
      
  • ${task.title}
  • `).join(''); // Attach event listeners document.querySelectorAll('.complete-btn').forEach(btn => { btn.addEventListener('click', () => { const id = parseInt(btn.dataset.id); taskService.completeTask(id); renderTasks(); }); }); document.querySelectorAll('.delete-btn').forEach(btn => { btn.addEventListener('click', () => { const id = parseInt(btn.dataset.id); taskService.deleteTask(id); renderTasks(); }); }); } taskForm.addEventListener('submit', event => { event.preventDefault(); const titleInput = taskForm.elements.title; const title = titleInput.value.trim(); if (title) { taskService.createTask(title); titleInput.value = ''; renderTasks(); } }); // Initial render renderTasks(); } // src/main.js import { initTaskUI } from './ui/TaskUI.js'; document.addEventListener('DOMContentLoaded', () => { initTaskUI(); });

    Browser Support and Using Modules

    Browser Support

    <!-- index.html -->
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>ES Modules Example</title>
    </head>
    <body>
      <div id="app"></div>
      
      <!-- Modern browsers support ES modules with type="module" -->
      <script type="module" src="./src/main.js"></script>
      
      <!-- Fallback for older browsers (requires a bundler) -->
      <script nomodule src="./dist/bundle.js"></script>
    </body>
    </html>
    

    Using Build Tools

    In practice, modules are usually processed by build tools like Webpack, Rollup, or Parcel, which handle bundling, tree-shaking, and more.

    // webpack.config.js
    module.exports = {
      entry: './src/main.js',
      output: {
        filename: 'bundle.js',
        path: __dirname + '/dist'
      },
      module: {
        rules: [
          {
            test: /\.js$/,
            exclude: /node_modules/,
            use: {
              loader: 'babel-loader',
              options: {
                presets: ['@babel/preset-env']
              }
            }
          }
        ]
      }
    };
    
    // package.json scripts
    {
      "scripts": {
        "build": "webpack --mode production",
        "dev": "webpack serve --mode development"
      }
    }
    

    Practice Exercises

    Exercise 1: Shopping Cart Module

    // Exercise: Create a shopping cart module system
    // 1. Create a Product class in a separate module
    // 2. Create a ShoppingCart class in another module
    // 3. Implement add, remove, and calculate total functions
    // 4. Export the classes and use them in a main.js file
    
    // Example starting point for Product.js:
    export class Product {
      constructor(id, name, price) {
        // Implement properties
      }
      
      // Implement methods
    }
    
    // Example starting point for ShoppingCart.js:
    export class ShoppingCart {
      constructor() {
        // Initialize cart
      }
      
      // Implement methods
    }
    
    // Test your implementation in main.js
    

    Exercise 2: Modular TodoList App

    // Exercise: Build a modular TodoList application
    // 1. Create modules for:
    //   - Todo model
    //   - TodoService (CRUD operations)
    //   - TodoUI (rendering and event handling)
    // 2. Use classes and private fields where appropriate
    // 3. Use proper module imports/exports
    // 4. Ensure the components are reusable
    
    // Example HTML to use:
    /*
    <!DOCTYPE html>
    <html lang="en">
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>Todo App</title>
      <link rel="stylesheet" href="styles.css">
    </head>
    <body>
      <div class="container">
        <h1>Todo List</h1>
        <form id="todoForm">
          <input type="text" id="todoInput" placeholder="Add a new task...">
          <button type="submit">Add</button>
        </form>
        <ul id="todoList"></ul>
      </div>
      
      <script type="module" src="./src/main.js"></script>
    </body>
    </html>
    
    */
    

    Key Takeaways

    Additional Resources