JavaScript Modules
JavaScript modules allow you to break up your code into separate files, making it easier to maintain, reuse, and distribute. Before ES6 (ECMAScript 2015), JavaScript did not have built-in support for modules, leading to community-driven solutions like CommonJS and AMD. Today, ES Modules (ESM) is the standard across modern browsers and Node.js.
Why Use Modules?
- Maintainability: By decoupling code, you can easily update, debug, and maintain individual pieces without affecting the entire application.
- Namespacing: Modules have their own scope. They prevent variables from polluting the global namespace, reducing naming collisions.
- Reusability: You can write a utility function once and import it anywhere.
ES Modules (ESM)
ES Modules use import and export statements. They are statically analyzed, meaning dependencies can be determined at compile time (enabling features like tree-shaking).
Exporting
You can export variables, functions, and classes from a module using named or default exports.
Named Exports
You can have multiple named exports per file.
// math.js
export const pi = 3.14159;
export function add(a, b) {
return a + b;
}
export class Calculator {
// ...
}
Or export them all at once at the bottom:
const pi = 3.14159;
function add(a, b) { return a + b; }
export { pi, add };
Default Exports
You can only have one default export per file. It's typically used when a module does one main thing (like a React component or a main utility class).
// logger.js
export default function logMessage(msg) {
console.log(`[LOG]: ${msg}`);
}
Importing
Importing Named Exports
When importing named exports, the names must match exactly, and they must be enclosed in curly braces.
import { pi, add } from './math.js';
console.log(add(pi, 2));
You can alias named imports using the as keyword:
import { add as sum } from './math.js';
Importing Default Exports
Default exports can be imported with any name and do not use curly braces.
import log from './logger.js';
log("Hello world!");
Mixed Imports
You can import both default and named exports in a single statement:
import log, { pi, add } from './utils.js';
Importing Everything
You can import all named exports as an object using * as:
import * as MathUtils from './math.js';
console.log(MathUtils.pi);
console.log(MathUtils.add(2, 3));
Dynamic Imports
Static imports (import ... from ...) are hoisted and evaluated before the module executes. However, sometimes you want to load a module conditionally or on-demand to improve performance (e.g., code splitting).
Dynamic imports return a Promise and can be used anywhere in the code.
const loadAnalytics = async () => {
if (userHasOptedIn) {
const analytics = await import('./analytics.js');
analytics.init();
}
};
Top-Level Await
In ES Modules, you can use the await keyword at the top level of a module outside of an async function. This is extremely useful for asynchronous initialization.
// db.js
import { connect } from 'db-driver';
export const connection = await connect('mongodb://localhost');
CommonJS vs ES Modules
If you work with Node.js, you will likely encounter CommonJS modules.
- CommonJS: Uses
require()for importing andmodule.exportsfor exporting. It is synchronous and dynamic. - ES Modules: Uses
importandexport. It is asynchronous (capable) and static.
// CommonJS Export
module.exports = function doSomething() {};
// CommonJS Import
const doSomething = require('./doSomething');
Modern Node.js supports ESM natively if you use the .mjs extension or set "type": "module" in your package.json.
Summary
Modules are the foundation of scalable JavaScript applications. They provide encapsulation, promote reusability, and power modern bundlers like Webpack, Vite, and Rollup to deliver optimized code to users.