Understanding Module Import and Export Patterns in CommonJS and ES6
When working with JavaScript module systems, understanding the interoperability and syntactic rules between CommonJS and ES6 (ES2015) modules is crucial for avoiding runtime errors and bundler conflicts. Below is a practical breakdown of export/import behaviors, syntax constraints, and architectural differences.
Named Exports and Import Syntax
When declaring a configuration object, avoid using reserved or generic identifiers like object or module to prevent namespace collisions and unexpected runtime behavior.
const appConfig = { theme: 'dark', version: '2.1.0' };
If you expose this variable using a named export:
export { appConfig };
The consuming file must use matching curly braces. Omitting them will result in a syntax error:
import { appConfig } from './config.js';
Aliasing during export changes the public interface. If you rename the export:
export { appConfig as globalSettings };
The original identifier is no longer accessible to importers. You must import using the alias:
import { globalSettings } from './config.js';
CommonJS Interoperability with ES6 Imports
When a module uses CommonJS syntax to expose a single value:
module.exports = appConfig;
Modern bundlers and Node.js allow you to consume it using a default ES6 import:
import appConfig from './config.js';
Attempting a named import like import { appConfig } from './config.js' will fail because CommonJS assigns the entire export to module.exports, not a named property. Naturally, the traditional CommonJS require syntax remains valid:
const appConfig = require('./config.js');
Conversely, if a module strictly uses ES6 exports (export { ... } or export default ...), you cannot reliably consume it using require() in standard environments. ES6 modules must be loaded via import.
Default Exports vs. Named Exports
Multiple named exports require explicit destructuring during import:
export const API_ENDPOINT = 'https://api.example.com';
export function initializeApp() {
console.log('System ready');
}
Importers must specify exactly what they need:
import { API_ENDPOINT, initializeApp } from './app.js';
Note that combining export default with a variable declaration like export default const x = 1; is invalid JavaScript syntax. Instead, declare the variable first and export it as the default:
const API_ENDPOINT = 'https://api.example.com';
export default API_ENDPOINT;
Default exports are imported with out braces, and each module is restricted to a single default export:
import API_ENDPOINT from './app.js';
Architectural Differences: ES6 Modules vs. CommonJS
The two module systems operate on fundamentally different principles:
- Export Capacity: ES6 supports multiple named exports alongside a single optional default export. CommonJS relies on
module.exports(single assignment, last one wins) orexports.property(multiple named properties). - Resolution Timing: ES6 module interfaces are statically analyzed and resolved at parse time. CommonJS resolves dependencies dynamical at runtime.
- Module Structure: ES6 modules are not plain objects; they are static bindings. CommonJS treats modules as objects that are fully loaded into memory.
- Loading Behavior: ES6 allows tree-shaking and partial imports of specific bindings. CommonJS loads the entire module object regardless of what is used.
- Value Binding: ES6 exports are live read-only references. If the original module mutates an exported variable, the importing module sees the updated value. CommonJS exports are shallow copies; subsequent changes in the source module do not affect already-imported values.
- Context (
this): In ES6 modules, the top-levelthisisundefined. In CommonJS,thisreferences the current module object (module.exports).