Los módulos ES (ESM) son el sistema oficial de módulos de JavaScript, estandarizado en ES2015. Permiten dividir el código en archivos separados con ámbito propio, importar y exportar solo lo necesario, y cargar dependencias de forma estática o dinámica. Hoy en día son el estándar tanto en navegadores como en Node.js.
Named exports y default export
Un módulo puede exportar múltiples named exports y un único default export. Ambos pueden coexistir en el mismo archivo:
// utils.js named exports
export function sumar(a, b) { return a + b; }
export function restar(a, b) { return a - b; }
export const PI = 3.14159;
export class Vector {
constructor(x, y) { this.x = x; this.y = y; }
magnitud() { return Math.sqrt(this.x ** 2 + this.y ** 2); }
}
// También se puede exportar al final:
function multiplicar(a, b) { return a * b; }
export { multiplicar };
// formato.js default export (solo uno por módulo)
export default function formatearMoneda(cantidad, moneda = 'EUR') {
return new Intl.NumberFormat('es-ES', {
style: 'currency',
currency: moneda
}).format(cantidad);
}
Importar: named, default, alias y namespace
// Named imports
import { sumar, restar, PI } from './utils.js';
console.log(sumar(2, 3)); // 5
// Alias para evitar colisiones
import { sumar as add, Vector as Vec } from './utils.js';
// Default import (el nombre puede ser cualquiera)
import formatearPrecio from './formato.js';
console.log(formatearPrecio(29.99)); // '29,99 '
// Combinar default y named
import formatearPrecio, { sumar, PI } from './modulo.js';
// Namespace import: todo en un objeto
import * as Matematicas from './utils.js';
console.log(Matematicas.sumar(1, 2)); // 3
console.log(Matematicas.PI); // 3.14159
Reexportación: crear puntos de entrada centralizados
Puedes reexportar desde un módulo índice para que los consumidores importen desde un único lugar:
// componentes/Boton.js
export default class Boton { ... }
export function crearBoton(config) { ... }
// componentes/Formulario.js
export default class Formulario { ... }
// componentes/index.js punto de entrada centralizado
export { default as Boton, crearBoton } from './Boton.js';
export { default as Formulario } from './Formulario.js';
export * from './helpers.js'; // Reexportar todo
// En el código de aplicación, importar desde el índice:
import { Boton, Formulario } from './componentes/index.js';
import() dinámico: lazy loading
import() como función devuelve una Promise y permite cargar módulos de forma dinámica, solo cuando se necesitan:
// Cargar un módulo solo cuando el usuario hace clic
boton.addEventListener('click', async () => {
const modulo = await import('./editor-pesado.js');
modulo.inicializar(contenedor);
});
// Código splitting: cargar la ruta solo cuando se navega
async function cargarRuta(ruta) {
switch (ruta) {
case '/dashboard':
const { Dashboard } = await import('./vistas/Dashboard.js');
return new Dashboard();
case '/perfil':
const { Perfil } = await import('./vistas/Perfil.js');
return new Perfil();
}
}
// Import condicional:
const modulo = await import(
isDev ? './utilidades-debug.js' : './utilidades.js'
);
Top-level await en módulos
En módulos ES, await puede usarse en el nivel superior del archivo. Los módulos que lo importen esperarán a que se resuelva:
// config.js top-level await
const respuesta = await fetch('/api/config');
export const config = await respuesta.json();
// app.js espera a que config.js esté listo
import { config } from './config.js';
console.log(config.tema); // Disponible sin await adicional
Soporte para navegadores antiguos: module/nomodule
El atributo type="module" solo lo entienden los navegadores modernos. El patrón module/nomodule permite servir código moderno a navegadores modernos y código compilado (bundles) a los antiguos:
// En HTML: // <script type="module" src="app.modern.js"></script> // <script nomodule src="app.legacy.js"></script> // Los navegadores modernos ignoran nomodule; los antiguos ignoran type="module" // Detectar si el entorno soporta módulos ES: const supModulos = 'noModule' in HTMLScriptElement.prototype;
Diferencias clave de los módulos ES respecto a los scripts normales: tienen su propio scope (no contaminan el global), se ejecutan en modo estricto automáticamente, los imports se resuelven de forma estática antes de ejecutar, y cada módulo se carga y evalúa solo una vez independientemente de cuántas veces se importe.
