Ir al contenido
angel.library

Módulos de JavaScript

Un módulo tiene scope propio, se ejecuta en strict mode y se evalúa una vez por identidad resuelta. Los imports estáticos se analizan antes de ejecutar, son bindings vivos y no pueden reasignarse desde el consumidor. import() carga bajo demanda y devuelve una Promise.

Un módulo tiene su propio scope y se evalúa una vez por URL. Usa exports nombrados para APIs explícitas y default solo cuando el módulo tiene una responsabilidad principal.

SintaxisQué expone o cargaCaso de uso
export const name = ...export nombradovarias piezas públicas
export { name as alias }export nombrado con otro nombreadaptar la API pública
export default valueun valor principalcomponente o responsabilidad central
import { name } from '...'binding nombrado vivodependencia estática
import value from '...'export defaultdependencia estática principal
import * as namespace from '...'objeto namespaceagrupar una API
import('...')Promise con el namespacecarga bajo demanda
format.js
export function formatName(name) {
return name.trim()
}
export const separator = ' '
// profile.js
import { formatName, separator } from './format.js'
formatName(' Ana ') // 'Ana'
separator // ' '

export default se importa con cualquier nombre, mientras que un export nombrado debe coincidir con su nombre o usar alias:

import formatDefault from "./format-default.js"
import { formatName as format } from "./format.js"
typeof format // 'function'
typeof formatDefault // depende del valor exportado por defecto

Mantén dependencias unidireccionales. Un módulo que importa de muchos lugares puede estar mezclando responsabilidades.

El import observa el valor actual exportado por el módulo, aunque el consumidor no pueda reasignarlo.

counter.js
export let count = 0
export function increment() {
count += 1
}
// app.js
import { count, increment } from './counter.js'
count // 0
increment()
count // 1
// count = 8 // TypeError: el import es de solo lectura

Los módulos se cargan por su specifier resuelto. En navegador, las rutas relativas necesitan normalmente extensión o un import map; un bundler puede aplicar reglas distintas.

Un index.js puede exponer una API pública estable:

export { createClient } from "./client.js"
export { ClientError } from "./client-error.js"

export type pertenece a TypeScript, no a JavaScript. Si el archivo es .ts, el compilador puede eliminar ese export porque solo existe durante el chequeo de tipos.

No reexportes cada detalle interno por comodidad. Un barrel demasiado amplio puede aumentar ciclos, hacer más difícil el tree-shaking y convertir rutas internas en API pública accidental.

import() devuelve una Promise y permite dividir código por ruta o interacción:

button.addEventListener("click", async () => {
const { openEditor } = await import("./editor.js")
openEditor()
})

El resultado completo de import() es un objeto namespace:

const module = await import("./format.js")
Object.keys(module) // ['formatName', 'separator']
module.formatName(" Eva ") // 'Eva'

Úsala cuando el módulo no sea necesario para el primer render. Maneja el fallo de red y el estado de carga; la división de bundle no elimina la necesidad de una UX clara.

<script
type="module"
src="/app.js"
></script>

Los scripts de módulo se difieren automáticamente, pueden importar otros módulos y siguen CORS. Cada <script type="module"> y cada dependencia se ejecuta una sola vez por identidad, aunque se solicite desde varios lugares.

Un módulo puede usar await en el nivel superior:

config.js
const response = await fetch("/config.json")
if (!response.ok) throw new Error(`HTTP ${response.status}`)
export const config = await response.json()

Los módulos dependientes esperan esa evaluación. Evita bloquear un grafo amplio con trabajo lento si la configuración puede cargarse de forma explícita después del arranque.

Los atributos declaran cómo debe interpretar el host un recurso. ECMAScript 2025 estandariza la sintaxis with y los JSON modules; la disponibilidad práctica depende del runtime y del bundler.

import config from "./config.json" with { type: "json" }
config.theme // valor exportado por defecto desde el JSON

También pueden combinarse con import dinámico:

const module = await import("./translations/es.json", {
with: { type: "json" }
})
module.default // objeto parseado

El atributo evita tratar JSON como JavaScript y hace explícito el tipo esperado. No reemplaza la validación de su estructura.

CommonJS usa require y module.exports; ESM usa import y export. Node puede convivir con ambos según package.json, extensión y configuración. Evita mezclar sintaxis sin entender qué formato genera el build. En librerías, documenta el formato de entrada y salida para no obligar al consumidor a adivinar.

Un ciclo A → B → A puede exponer un binding antes de que esté inicializado. Si dos módulos necesitan datos de ambos, extrae el contrato común o invierte la dependencia. Mantén inicialización global —listeners, conexión a DB, lectura de env— en un punto de composición, no como efecto lateral inesperado de cualquier import.

Antes: feature-a.js → feature-b.js → feature-a.js
Después: feature-a.js → shared-contract.js ← feature-b.js

import.meta contiene metadatos definidos por el host. En el navegador y varios runtimes, import.meta.url identifica el módulo actual y permite resolver recursos relativos sin depender de la URL de la página.

const workerURL = new URL("./search.worker.js", import.meta.url)
const worker = new Worker(workerURL, { type: "module" })
workerURL // URL absoluta resuelta junto al módulo actual

Propiedades como import.meta.env, import.meta.glob o import.meta.dirname no son universales: dependen del bundler o runtime. Documenta esa frontera antes de mover el módulo a otra herramienta.

Un import puede hacer que todo su grafo viaje al navegador en herramientas con bundling. No importes un módulo con secretos desde código cliente aunque solo uses una función aparentemente inocente; separa módulos server-only y APIs públicas. La frontera de módulos es también una frontera de seguridad y de tamaño de bundle.

// app.js: no conoce archivos internos
import { PaymentError, createCheckout } from "./payments/index.js"
// payments/index.js: contrato público
export { createCheckout } from "./create-checkout.js"
export { PaymentError } from "./payment-error.js"

El consumidor depende de una frontera estable. Los helpers internos pueden cambiar de ubicación sin obligar a modificar cada import de la aplicación.