ES Modules: Sistema de Módulos de JavaScript
Domina el sistema de módulos nativo de JavaScript introducido en ES6. Aprende todas las formas de import y export, cómo funcionan en navegadores y Node.js, y características avanzadas como live bindings.
TL;DR - Resumen rápido
- ES Modules es el sistema de módulos nativo y estándar de JavaScript desde ES6
- export expone funcionalidad desde un módulo (named export, default export, export list)
- import consume funcionalidad de otros módulos de forma declarativa y estática
- Los módulos se cargan en navegadores con <script type='module'> y en Node.js con extensión .mjs o type:module
- Los imports crean live bindings: cambios en la variable exportada se reflejan en todos los módulos que la importan
Introducción a ES Modules
ES Modules (ESM) es el sistema de módulos oficial de JavaScript, introducido en ES6 (2015) y ahora el estándar universal para organizar código JavaScript moderno. A diferencia de sistemas anteriores como CommonJS o AMD, ES Modules está integrado directamente en el lenguaje y soportado nativamente por todos los navegadores modernos y Node.js.
La sintaxis de ES Modules se basa en dos palabras clave fundamentales: export para exponer funcionalidad desde un módulo, e import para consumir esa funcionalidad en otro módulo. Esta sintaxis es declarativa y estática, lo que significa que las importaciones y exportaciones se determinan en tiempo de análisis, no en tiempo de ejecución.
Sintaxis estática vs dinámica
La sintaxis estática de ES Modules permite que las herramientas de bundling analicen el código antes de ejecutarlo, identificando qué módulos se usan y cuáles no. Esto hace posible optimizaciones como tree shaking (eliminar código no usado) y code splitting (dividir código en chunks), mejorando significativamente el rendimiento de las aplicaciones.
Sintaxis de export
La palabra clave export permite exponer funciones, clases, objetos, constantes o variables desde un módulo para que puedan ser importados en otros módulos. Hay varias formas de exportar en ES Modules, cada una con casos de uso específicos.
Export individual (named export)
La forma más común de exportar es usando export directamente antes de la declaración. Esto crea una exportación con nombre (named export) que debe importarse usando el mismo nombre.
Cada elemento exportado tiene un nombre específico que debe usarse al importar. Puedes exportar múltiples elementos del mismo módulo, y los módulos que importen pueden elegir qué elementos necesitan. Esta es la forma más común y recomendada de exportar en ES Modules. Prefiere named exports sobre default exports cuando exportas múltiples elementos: los named exports son más explícitos, facilitan el autocompletado en IDEs, y hacen que los refactorings sean más seguros porque el nombre debe coincidir exactamente.
Export en lista
Puedes declarar todas tus funciones, clases y variables primero, y luego exportarlas en una lista al final del archivo. Esta forma es útil cuando quieres tener un control claro de qué se exporta desde el módulo.
Esta sintaxis es especialmente útil cuando tienes funciones privadas (helper functions) que no quieres exportar. Declaras todo el código del módulo normalmente, y al final del archivo decides explícitamente qué exponer. Esto hace que la interfaz pública del módulo sea más clara.
Export default
Cada módulo puede tener una exportación por defecto usando export default. La exportación default es útil cuando el módulo exporta principalmente una sola funcionalidad, como una clase o función principal.
La diferencia clave con default export es que al importar no necesitas usar llaves y puedes elegir cualquier nombre. Sin embargo, esto puede causar inconsistencias en el código si diferentes archivos usan nombres diferentes para la misma importación. Por esta razón, muchos equipos prefieren usar solo named exports.
Default export: úsalo con cuidado
Aunque default export es común en React y otras librerías, puede causar problemas de mantenibilidad. Como puedes importar con cualquier nombre, diferentes partes del código pueden usar nombres diferentes para el mismo módulo, dificultando las búsquedas y refactorings. Considera usar solo named exports para mayor consistencia.
Sintaxis de import
La palabra clave import permite consumir las exportaciones de otros módulos. La sintaxis varía según cómo se haya exportado el módulo y qué necesites importar.
Import de named exports
Para importar named exports, usas llaves con los nombres exactos de las exportaciones. Puedes importar uno o varios elementos del mismo módulo, y solo importas lo que necesitas.
Esta sintaxis es muy explícita: ves exactamente qué estás importando de cada módulo. Solo el código que importas se incluye en tu bundle final (gracias a tree shaking), lo que mejora el rendimiento. También puedes renombrar importaciones usando as si hay conflictos de nombres.
Import de default export
Para importar una exportación default, no usas llaves y puedes elegir cualquier nombre para la importación. Puedes combinar imports default y named en la misma declaración.
Al importar un default export, puedes usar el nombre que prefieras. Esto es conveniente pero puede causar inconsistencias si diferentes archivos usan nombres diferentes. También puedes combinar el import default con named imports en la misma línea, lo cual es común en librerías como React.
Import namespace (import *)
Puedes importar todas las exportaciones de un módulo como un objeto namespace usando import * as nombre. Esto es útil cuando un módulo exporta muchos elementos relacionados que quieres agrupar bajo un nombre común.
El namespace import crea un objeto que contiene todas las exportaciones del módulo. Esto es útil para módulos con muchas exportaciones relacionadas (como módulos de utilidades o APIs), pero puede dificultar el tree shaking porque el bundler no puede determinar fácilmente qué métodos del namespace realmente se usan. Aunque namespace imports son convenientes, prefiere importar solo lo que necesitas con destructuring (import { a, b }) en lugar de importar todo (import * as todo), ya que esto permite que el bundler elimine código no usado más efectivamente.
Módulos en Node.js
Node.js soporta ES Modules desde la versión 12, aunque por defecto usa el sistema CommonJS. Para usar ES Modules en Node.js, tienes dos opciones: usar la extensión .mjs para tus archivos, o agregar "type": "module" en tu package.json.
Una vez configurado, Node.js trata tus archivos como ES Modules y puedes usar import/export normalmente. Sin embargo, hay diferencias importantes con CommonJS: no tienes acceso a __dirname y __filename (debes usar import.meta.url), los imports deben incluir la extensión del archivo, y el soporte para JSON requiere import assertions.
Migración gradual de CommonJS a ESM
Puedes migrar gradualmente de CommonJS a ES Modules usando la extensión .mjs para archivos nuevos mientras mantienes archivos .js con CommonJS. Node.js puede mezclar ambos sistemas en el mismo proyecto, permitiendo una transición progresiva.
Características avanzadas de ES Modules
ES Modules tiene características avanzadas que lo diferencian de otros sistemas de módulos. Entender estas características es crucial para escribir código modular robusto y aprovechar al máximo el sistema de módulos.
Live bindings (enlaces vivos)
Una característica única de ES Modules es que los imports son "live bindings" (enlaces vivos). Esto significa que si el valor exportado cambia en el módulo original, ese cambio se refleja automáticamente en todos los módulos que lo importan. Esto es diferente de CommonJS, donde se copia el valor.
Los live bindings son extremadamente útiles para valores que cambian con el tiempo, como configuraciones, estado de la aplicación, o contadores. Sin embargo, es importante notar que los imports son de solo lectura: puedes ver los cambios del módulo exportador, pero no puedes modificar el valor importado directamente desde el módulo importador.
Imports de solo lectura
Aunque los imports son live bindings, son de solo lectura. Si intentas reasignar una variable importada, obtendrás un error en modo estricto. Solo el módulo que exporta puede modificar el valor. Esto ayuda a prevenir efectos secundarios no deseados y mantiene el flujo de datos más predecible.
Hoisting de imports
Los imports en ES Modules se "elevan" (hoist) al inicio del módulo, lo que significa que se procesan antes que cualquier otro código del módulo. Esto hace que puedas usar importaciones incluso antes de declararlas en el código, aunque la mejor práctica es declarar imports al inicio del archivo.
El hoisting de imports tiene implicaciones importantes: los módulos se evalúan de forma estática antes de ejecutar cualquier código, todas las dependencias se cargan primero, y el orden de los imports en el archivo no afecta el orden de ejecución. Esto hace que el sistema de módulos sea más predecible y fácil de analizar.
Resumen: ES Modules
Conceptos principales:
- •ES Modules es el sistema de módulos nativo y estándar de JavaScript desde ES6
- •export expone funcionalidad (named exports, default export, export lists)
- •import consume funcionalidad de forma declarativa y estática
- •Los módulos se cargan con <script type='module'> en navegadores
- •En Node.js usa extensión .mjs o 'type: module' en package.json
- •Los imports son live bindings de solo lectura que reflejan cambios del exportador
Mejores prácticas:
- •Prefiere named exports sobre default exports para mayor consistencia
- •Importa solo lo que necesitas para facilitar tree shaking
- •Declara todos los imports al inicio del archivo por claridad
- •Usa namespace imports solo cuando agrupar tiene sentido semántico
- •Evita modificar valores importados, respeta la inmutabilidad
- •Sirve módulos desde un servidor local en desarrollo (no file://)