Command Palette

Search for a command to run...

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.

Lectura: 15 min
Nivel: Intermedio

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.

export-individual.js
Loading code...

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.

export-lista.js
Loading code...

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.

export-default.js
Loading code...

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.

import-named.js
Loading code...

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.

import-default.js
Loading code...

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.

import-namespace.js
Loading code...

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 el navegador

Los navegadores modernos soportan ES Modules nativamente. Para cargar un módulo en el navegador, usas la etiqueta <script> con el atributo type="module". Esto indica al navegador que el script debe tratarse como un módulo ES6.

modulos-navegador.html
Loading code...

Los scripts de tipo module tienen comportamientos especiales: se ejecutan en modo estricto por defecto, tienen su propio scope (no contaminan el scope global), se cargan de forma asíncrona (no bloquean el rendering), y soportan imports de otros módulos. Los módulos se cargan solo una vez, incluso si múltiples scripts los importan.

  • <strong>Modo estricto automático:</strong> Los módulos siempre se ejecutan en modo estricto sin necesidad de 'use strict'
  • <strong>Scope propio:</strong> Las variables del módulo no se agregan al scope global (window)
  • <strong>Carga diferida:</strong> Los módulos se ejecutan después de que el HTML se parsea (similar a defer)
  • <strong>CORS habilitado:</strong> Los módulos se cargan con CORS, por lo que deben servirse desde el mismo origen o con headers CORS
  • <strong>Una sola ejecución:</strong> Cada módulo se ejecuta solo una vez, sin importar cuántas veces se importe

Desarrollo local y CORS

No puedes abrir archivos HTML con módulos directamente con file:// en el navegador debido a restricciones CORS. Necesitas usar un servidor local (como Live Server en VS Code, o python -m http.server) para servir los archivos y probar módulos en desarrollo.

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.

modulos-nodejs.js
Loading code...

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.

live-bindings.js
Loading code...

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.

hoisting-imports.js
Loading code...

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://)