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
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
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
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.
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
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
- Classes provide a familiar syntax for object-oriented programming
- Under the hood, JavaScript classes still use prototype-based inheritance
- Private fields and methods (with #) provide true encapsulation
- Modules help organize code into reusable, maintainable units
- Modules have their own scope, preventing global namespace pollution
- Different export types serve different purposes (named vs default)
- Dynamic imports enable code splitting and lazy loading
- Classes and modules work together to create maintainable applications
- Building tools like Webpack enhance module capabilities with bundling