Modules & Classes
Learn ES6 module syntax for code organization and class syntax for object-oriented programming in JavaScript.
Import and Export
ES6 modules provide a native module system for JavaScript with static import and export declarations. Named exports allow multiple values to be exported from a module, and consumers import them using curly braces with matching names. Modules are evaluated once and cached, so importing the same module from different files returns the same instance. Module code runs in strict mode by default and has its own scope, preventing global namespace pollution.
// math.js - Named exports
export const PI = 3.14159;
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
export class Vector {
constructor(x, y) {
this.x = x;
this.y = y;
}
magnitude() {
return Math.sqrt(this.x ** 2 + this.y ** 2);
}
}
// app.js - Named imports
import { add, multiply, PI, Vector } from "./math.js";
console.log(add(2, 3)); // 5
console.log(PI); // 3.14159
// Rename on import
import { add as sum, multiply as mul } from "./math.js";
console.log(sum(1, 2)); // 3
// Import everything as namespace
import * as math from "./math.js";
console.log(math.add(5, 3)); // 8
console.log(math.PI); // 3.14159
Default Exports
Each module can have at most one default export, which represents the primary value the module provides. Default exports are imported without curly braces and can be given any name by the importer. The choice between named and default exports depends on whether the module provides a single main thing or a collection of utilities. A common convention is to use default exports for classes and components, and named exports for utility functions and constants.
// logger.js - Default export
export default class Logger {
constructor(prefix) {
this.prefix = prefix;
}
info(msg) {
console.log(`[${this.prefix}] INFO: ${msg}`);
}
error(msg) {
console.error(`[${this.prefix}] ERROR: ${msg}`);
}
warn(msg) {
console.warn(`[${this.prefix}] WARN: ${msg}`);
}
}
// app.js - Import default (any name works)
import Logger from "./logger.js";
const log = new Logger("App");
log.info("Started");
// Combine default and named exports
// api.js
export default class ApiClient {
constructor(baseUrl) {
this.baseUrl = baseUrl;
}
async get(path) {
const res = await fetch(`${this.baseUrl}${path}`);
return res.json();
}
}
export const BASE_URL = "https://api.example.com";
export function createClient(url = BASE_URL) {
return new ApiClient(url);
}
// consumer.js
import ApiClient, { BASE_URL, createClient } from "./api.js";
const client = createClient();
Class Syntax
ES6 classes provide a cleaner syntax for creating constructor functions and setting up prototypal inheritance. Under the hood, classes are still functions and prototypes — they are syntactic sugar, not a new object model. The constructor method initializes new instances, and methods defined in the class body are added to the prototype. Classes support getters and setters for computed properties, and static methods that belong to the class itself rather than instances.
class EventEmitter {
#listeners = new Map(); // Private field
on(event, callback) {
if (!this.#listeners.has(event)) {
this.#listeners.set(event, []);
}
this.#listeners.get(event).push(callback);
return this;
}
emit(event, ...args) {
const callbacks = this.#listeners.get(event) || [];
callbacks.forEach((cb) => cb(...args));
return this;
}
off(event, callback) {
const callbacks = this.#listeners.get(event) || [];
this.#listeners.set(
event,
callbacks.filter((cb) => cb !== callback)
);
return this;
}
get eventNames() {
return [...this.#listeners.keys()];
}
static create() {
return new EventEmitter();
}
}
const emitter = EventEmitter.create();
emitter
.on("data", (msg) => console.log("Received:", msg))
.on("error", (err) => console.error("Error:", err));
emitter.emit("data", "Hello!");
console.log("Events:", emitter.eventNames);
Inheritance with extends
The extends keyword creates a subclass that inherits from a parent class, establishing a prototype chain between the child and parent prototypes. The super keyword calls the parent class constructor and is required in the child constructor before using 'this'. Child classes can override parent methods and call the parent's version using super.methodName(). This pattern enables building class hierarchies while keeping the code organized and readable.
class Shape {
constructor(color = "black") {
this.color = color;
}
describe() {
return `A ${this.color} ${this.constructor.name}`;
}
area() {
throw new Error("area() must be implemented by subclass");
}
}
class Circle extends Shape {
constructor(radius, color) {
super(color); // Must call super before using 'this'
this.radius = radius;
}
area() {
return Math.PI * this.radius ** 2;
}
describe() {
return `${super.describe()} with radius ${this.radius}`;
}
}
class Rectangle extends Shape {
constructor(width, height, color) {
super(color);
this.width = width;
this.height = height;
}
area() {
return this.width * this.height;
}
}
const shapes = [
new Circle(5, "red"),
new Rectangle(4, 6, "blue"),
];
for (const shape of shapes) {
console.log(shape.describe());
console.log(`Area: ${shape.area().toFixed(2)}`);
}
// instanceof checks
console.log(shapes[0] instanceof Circle); // true
console.log(shapes[0] instanceof Shape); // true