ES6 Modules
Welcome to the world of ES6 modules! In this lesson, we'll explore JavaScript's native module system that allows you to organize and reuse code across files. Modules are essential for building scalable, maintainable applications.
What are ES6 Modules?
ES6 modules provide a standardized way to organize JavaScript code into separate files and import/export functionality between them:
Key Benefits: Modules enable code reusability, better organization, namespace management, and dependency management. Each module has its own scope, preventing global namespace pollution.
Basic Export Syntax
There are two ways to export from a module: named exports and default exports.
Named Exports (math.js):
// Export individual items
export const PI = 3.14159;
export const E = 2.71828;
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
// Or export multiple items at once
const subtract = (a, b) => a - b;
const divide = (a, b) => a / b;
export { subtract, divide };
Default Exports
Each module can have one default export, typically used for the main functionality of the module:
Default Export (calculator.js):
// Option 1: Export default directly
export default class Calculator {
add(a, b) {
return a + b;
}
subtract(a, b) {
return a - b;
}
}
// Option 2: Export default separately
class Calculator {
// ...
}
export default Calculator;
// Option 3: Export default anonymous
export default function(a, b) {
return a + b;
}
Best Practice: Use named exports when exporting multiple items, and default exports for the primary export of a module. You can combine both in the same file.
Basic Import Syntax
Import functionality from other modules using the import keyword:
Importing Named Exports:
// Import specific named exports
import { add, multiply } from './math.js';
console.log(add(5, 3)); // 8
console.log(multiply(4, 2)); // 8
// Import all named exports as an object
import * as math from './math.js';
console.log(math.add(5, 3)); // 8
console.log(math.PI); // 3.14159
console.log(math.multiply(4, 2)); // 8
Importing Default Exports:
// Import default export (name can be anything)
import Calculator from './calculator.js';
const calc = new Calculator();
console.log(calc.add(10, 5)); // 15
// Import both default and named exports
import Calculator, { PI, E } from './calculator.js';
Renaming Imports and Exports
Use the as keyword to rename imports or exports:
Renaming Exports:
// utils.js
const calculate = (a, b) => a + b;
const format = (str) => str.toUpperCase();
export {
calculate as sum,
format as uppercase
};
Renaming Imports:
// app.js
import { sum as add, uppercase as toUpper } from './utils.js';
console.log(add(5, 3)); // 8
console.log(toUpper('hello')); // HELLO
// Useful for avoiding naming conflicts
import { format as formatString } from './string-utils.js';
import { format as formatNumber } from './number-utils.js';
Re-exporting from Modules
Create an index file that aggregates and re-exports from multiple modules:
Re-exporting Pattern (utils/index.js):
// Re-export everything from other modules
export * from './math.js';
export * from './string.js';
// Re-export specific named exports
export { add, subtract } from './math.js';
export { capitalize } from './string.js';
// Re-export and rename
export { multiply as times } from './math.js';
// Re-export default as named
export { default as Calculator } from './calculator.js';
Using Re-exported Modules:
// Now you can import from a single file
import { add, subtract, capitalize, Calculator } from './utils/index.js';
// Or use the directory name (if index.js exists)
import { add, subtract } from './utils';
Dynamic Imports
Load modules dynamically at runtime using the import() function, which returns a Promise:
Dynamic Import Syntax:
// Load module on demand
async function loadCalculator() {
const module = await import('./calculator.js');
const Calculator = module.default;
const calc = new Calculator();
console.log(calc.add(5, 3)); // 8
}
// With named exports
async function loadMath() {
const { add, multiply } = await import('./math.js');
console.log(add(2, 3)); // 5
console.log(multiply(4, 5)); // 20
}
// Conditional loading
if (condition) {
import('./heavy-module.js').then(module => {
module.doSomething();
});
}
Use Case: Dynamic imports are perfect for code splitting, lazy loading features, and loading modules based on user actions or conditions.
Module Scope and Behavior
Understanding how modules behave is crucial for effective use:
Module Characteristics:
1. Modules have their own scope
- Variables are not global
- Top-level 'this' is undefined
2. Modules are singletons
- Code executes once on first import
- Same instance shared across imports
3. Imports are read-only
- Cannot reassign imported bindings
- But can modify object properties
4. Modules are deferred
- Similar to scripts with defer attribute
- Execute after HTML parsing
Example - Module Singleton:
// counter.js
let count = 0;
export function increment() {
return ++count;
}
export function getCount() {
return count;
}
// app.js
import { increment, getCount } from './counter.js';
console.log(increment()); // 1
console.log(increment()); // 2
// another-file.js
import { getCount } from './counter.js';
console.log(getCount()); // 2 (same count, shared state)
Module Patterns and Best Practices
Here are common patterns for organizing modules:
1. One Class Per File (user.js):
export default class User {
constructor(name, email) {
this.name = name;
this.email = email;
}
getInfo() {
return `${this.name} (${this.email})`;
}
}
2. Related Functions (array-utils.js):
export function chunk(array, size) {
// Implementation
}
export function flatten(array) {
// Implementation
}
export function unique(array) {
return [...new Set(array)];
}
3. Configuration Object (config.js):
export default {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3
};
4. Factory Pattern (create-logger.js):
export default function createLogger(name) {
return {
log: (msg) => console.log(`[${name}] ${msg}`),
error: (msg) => console.error(`[${name}] ${msg}`)
};
}
Important: Module files must be served with the correct MIME type (text/javascript) and require a web server. They won't work with the file:// protocol in most browsers.
Using Modules in HTML
Include modules in your HTML using the type="module" attribute:
HTML Module Usage:
<!DOCTYPE html>
<html>
<head>
<title>ES6 Modules Example</title>
</head>
<body>
<h1>Module Demo</h1>
<!-- Main module entry point -->
<script type="module" src="app.js"></script>
<!-- Inline module -->
<script type="module">
import { add } from './math.js';
console.log(add(2, 3));
</script>
<!-- Fallback for browsers without module support -->
<script nomodule src="bundle.js"></script>
</body>
</html>
Real-World Example: User Management System
Let's build a practical example with multiple modules:
models/user.js:
export default class User {
constructor(id, name, email) {
this.id = id;
this.name = name;
this.email = email;
}
toString() {
return `User #${this.id}: ${this.name}`;
}
}
services/user-service.js:
import User from '../models/user.js';
class UserService {
constructor() {
this.users = [];
this.nextId = 1;
}
create(name, email) {
const user = new User(this.nextId++, name, email);
this.users.push(user);
return user;
}
findById(id) {
return this.users.find(u => u.id === id);
}
getAll() {
return [...this.users];
}
}
export default new UserService(); // Export singleton instance
utils/validator.js:
export function validateEmail(email) {
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
export function validateName(name) {
return name && name.length >= 2;
}
app.js:
import userService from './services/user-service.js';
import { validateEmail, validateName } from './utils/validator.js';
function createUser(name, email) {
if (!validateName(name)) {
throw new Error('Invalid name');
}
if (!validateEmail(email)) {
throw new Error('Invalid email');
}
return userService.create(name, email);
}
// Usage
try {
const user1 = createUser('John Doe', 'john@example.com');
const user2 = createUser('Jane Smith', 'jane@example.com');
console.log(userService.getAll());
console.log(userService.findById(1)); // John Doe
} catch (error) {
console.error(error.message);
}
Practice Exercise:
Task: Create a modular shopping cart system with:
- A
Product class in models/product.js
- A
Cart class in services/cart.js with add, remove, getTotal methods
- Utility functions in utils/currency.js to format prices
- An app.js that uses all modules
Solution:
models/product.js:
export default class Product {
constructor(id, name, price) {
this.id = id;
this.name = name;
this.price = price;
}
}
services/cart.js:
export default class Cart {
constructor() {
this.items = [];
}
add(product, quantity = 1) {
const existingItem = this.items.find(i => i.product.id === product.id);
if (existingItem) {
existingItem.quantity += quantity;
} else {
this.items.push({ product, quantity });
}
}
remove(productId) {
this.items = this.items.filter(i => i.product.id !== productId);
}
getTotal() {
return this.items.reduce((sum, item) => {
return sum + (item.product.price * item.quantity);
}, 0);
}
getItems() {
return [...this.items];
}
}
utils/currency.js:
export function formatPrice(amount, currency = 'USD') {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency
}).format(amount);
}
app.js:
import Product from './models/product.js';
import Cart from './services/cart.js';
import { formatPrice } from './utils/currency.js';
const cart = new Cart();
const laptop = new Product(1, 'Laptop', 999.99);
const mouse = new Product(2, 'Mouse', 29.99);
cart.add(laptop, 1);
cart.add(mouse, 2);
console.log('Cart Items:', cart.getItems());
console.log('Total:', formatPrice(cart.getTotal())); // $1,059.97
Summary
In this lesson, you learned:
- ES6 modules provide a native way to organize code into separate files
- Use named exports for multiple exports and default exports for main exports
- Import modules using import statements with various syntax options
- Dynamic imports allow loading modules on demand with import()
- Modules have their own scope and are singleton instances
- Use type="module" in HTML script tags to use ES6 modules
- Module patterns help organize code for scalability and maintainability
Next Up: In the next lesson, we'll explore Module Bundlers like Webpack and understand how they optimize module-based applications!