Exportar e importar funciones en JavaScript (import/export)
Para importar y exportar funciones en JavaScript se usan export en el fichero que las define e import en el que las usa. Cuando un programa crece, lo repartes en varios ficheros. Cada fichero es un módulo: como una caja de herramientas. Lo que no exportas se queda dentro de la caja; lo que exportas queda en la bandeja de fuera, y otros ficheros pueden cogerlo con import.
Este sistema se llama módulos ES (de ECMAScript, el estándar de JavaScript) y funciona igual en el navegador y en Node. Todo el código de este artículo se ha ejecutado con Node 24.21.0 y, el del navegador, con Google Chrome 157 sin interfaz.
Cómo exportar una función
Hay dos formas: con nombre, de las que puede haber varias por fichero, y por defecto, una como mucho.
// src/precios.js
export const IVA_GENERAL = 0.21;
export function conIva(precio) {
return precio * (1 + IVA_GENERAL);
}
function redondear(cantidad) {
return Math.round(cantidad * 100) / 100;
}
export { redondear as redondearCentimos };
Este fichero exporta tres cosas: IVA_GENERAL, conIva y redondear, que sale de la caja con otro nombre, redondearCentimos. Puedes poner export delante de cada declaración o reunir las exportaciones al final con export { … }; el resultado es el mismo.
// src/formatearPrecio.js
export default function formatearPrecio(cantidad) {
return `${cantidad.toFixed(2)} €`;
}
Cómo importar una función
// main.js
import formatear from "./src/formatearPrecio.js";
import { conIva, redondearCentimos as redondear } from "./src/precios.js";
import * as precios from "./src/precios.js";
console.log(formatear(redondear(conIva(10))));
console.log(precios.IVA_GENERAL);
console.log(Object.keys(precios));
node main.js
12.10 €
0.21
[ 'IVA_GENERAL', 'conIva', 'redondearCentimos' ]
(Para que Node acepte import en un .js, el proyecto lleva "type": "module" en su package.json; lo ves más abajo.)
| Quieres… | Exportas | Importas |
|---|---|---|
| Varias cosas de un fichero | export function a() {} |
import { a, b } from "./f.js" |
| Una cosa principal | export default function a() {} |
import a from "./f.js" |
| Cambiar el nombre al importar | export function a() {} |
import { a as otra } from "./f.js" |
| Todo el módulo en un objeto | cualquiera | import * as f from "./f.js" |
| Cargarlo solo cuando haga falta | cualquiera | const f = await import("./f.js") |
Export default frente a export con nombre
| Con nombre | Por defecto | |
|---|---|---|
| Cuántos por fichero | Los que quieras | Uno como mucho |
| Al importar | Entre llaves y con el mismo nombre (o as) |
Sin llaves y con el nombre que elijas |
| Si te equivocas de nombre | Error al cargar | No hay nombre que equivocar |
La última fila se ve al ejecutar. Una errata en el nombre (conIVA en lugar de conIva) se detecta antes de que corra una sola línea:
SyntaxError: The requested module './src/precios.js' does not provide an export named 'conIVA'
Y el error es el mismo si pides con llaves algo que se exportó por defecto:
import { formatearPrecio } from "./src/formatearPrecio.js";
SyntaxError: The requested module './src/formatearPrecio.js' does not provide an export named 'formatearPrecio'
La función se llama formatearPrecio dentro del fichero, pero se exportó como default. Se importa sin llaves: import formatearPrecio from "./src/formatearPrecio.js".
Con export default, en cambio, cada fichero que la importa puede llamarla de una forma distinta (formatear, formato, fp), y buscar sus usos cuesta más. Por eso hay equipos que prefieren las exportaciones con nombre, y otros que reservan default para lo principal del fichero, como un componente. Las dos son correctas; lo útil es que el proyecto elija un criterio (y esto es opinión nuestra, no una regla del lenguaje).
Importar un módulo en HTML con script type="module"
En el navegador, el fichero principal se carga con type="module":
<!doctype html>
<html lang="es">
<head>
<meta charset="utf-8">
<title>Módulos en el navegador</title>
<script type="module" src="web.js"></script>
</head>
<body>
<p id="precio"></p>
<p id="formato"></p>
</body>
</html>
// web.js
import formatear from "./src/formatearPrecio.js";
import { conIva, redondearCentimos } from "./src/precios.js";
document.querySelector("#precio").textContent = formatear(redondearCentimos(conIva(10)));
// Carga diferida: el módulo solo se descarga cuando hace falta
const { default: formatearOtraVez } = await import("./src/formatearPrecio.js");
document.querySelector("#formato").textContent = formatearOtraVez(3);
Servido con un servidor local (python3 -m http.server) y abierto en Chrome, la página queda así:
<p id="precio">12.10 €</p>
<p id="formato">3.00 €</p>
Los mismos precios.js y formatearPrecio.js sirven para Node y para el navegador, sin cambiar nada. Tres cosas que conviene saber:
- Sin
type="module"falla. Con un<script src="web.js">normal, la consola muestraUncaught SyntaxError: Cannot use import statement outside a moduley no se ejecuta nada. - No funciona desde
file://. Si abres el HTML con doble clic, Chrome bloquea el módulo:has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: … http, https…. MDN lo explica: las peticiones de módulos usan CORS y las URLfile://nunca son del mismo origen. Necesitas un servidor local, aunque sea el de Python o el de tu editor. - No hace falta
defer. Según la misma página, los scripts de módulo se aplazan solos hasta que el HTML está leído, y cada módulo se ejecuta una sola vez aunque lo importen varios ficheros.
En el navegador, la ruta empieza por ./, ../ o /, o es una URL completa. Un nombre suelto como import { x } from "precios" da un TypeError, salvo que lo traduzcas a una ruta con un import map (<script type="importmap">). Las herramientas como Vite o webpack resuelven esos nombres por ti.
Importar en Node: .mjs y "type": "module"
Node decide si un .js es módulo ES o CommonJS mirando el package.json más cercano:
{
"name": "modulos-es",
"type": "module"
}
| Fichero | Cómo lo trata Node |
|---|---|
.mjs |
Siempre módulo ES |
.cjs |
Siempre CommonJS |
.js con "type": "module" |
Módulo ES |
.js con "type": "commonjs" |
CommonJS |
.js sin "type" |
CommonJS, salvo que el código use sintaxis de módulo ES |
La última fila es la detección de sintaxis de Node. Funciona, pero si el package.json no tiene el campo "type", Node 24 avisa en cada ejecución:
(node:2154902) [MODULE_TYPELESS_PACKAGE_JSON] Warning: Module type of file:///…/sin-type/main.js is not specified and it doesn't parse as CommonJS.
Reparsing as ES module because module syntax was detected. This incurs a performance overhead.
To eliminate this warning, add "type": "module" to /…/sin-type/package.json.
Si no quieres tocar el package.json, usa la extensión .mjs, que funciona incluso sin él:
// saludo.mjs
export const saludo = "hola";
// main.mjs
import { saludo } from "./saludo.mjs";
console.log(saludo);
hola
Dos errores típicos al pasar a módulos ES en Node:
- Olvidar la extensión.
import { conIva } from "./src/precios"falla conError [ERR_MODULE_NOT_FOUND]: Cannot find module '…/src/precios' imported from …/error-extension.js. La documentación de Node exige la extensión, y tambiénindex.jssi importas una carpeta. Sin extensión solo funciona si una herramienta (Vite, webpack, Jest) resuelve la ruta por ti. - Seguir usando
require. En un módulo ES no existe:ReferenceError: require is not defined in ES module scope, you can use import instead. Lo mismo pasa conmodule,__filenamey__dirname; los dos últimos se sustituyen porimport.meta.filenameeimport.meta.dirname.
Import dinámico: import()
import arriba del fichero carga el módulo siempre. import() lo carga cuando llega a esa línea y devuelve una promesa: sirve para cargar algo solo si hace falta.
// dinamico.js
const quiereFormato = process.argv[2] === "--formato";
if (quiereFormato) {
const modulo = await import("./src/formatearPrecio.js");
console.log(modulo.default(12.1));
} else {
console.log("Sin cargar el módulo de formato");
}
node dinamico.js
node dinamico.js --formato
Sin cargar el módulo de formato
12.10 €
Lo exportado por defecto llega en la propiedad default. El await fuera de una función solo vale en módulos ES; en un fichero CommonJS usarías import("./f.js").then((modulo) => …). import() funciona tanto en módulos ES como en CommonJS, y permite cargar un módulo desde un <script> sin type="module".
CommonJS: require y module.exports
Antes de los módulos ES, Node tenía su propio sistema, CommonJS, y sigue en muchos proyectos y paquetes. En un proyecto con "type": "module", los ficheros CommonJS llevan la extensión .cjs:
// precios.cjs (CommonJS)
const IVA_GENERAL = 0.21;
function conIva(precio) {
return precio * (1 + IVA_GENERAL);
}
module.exports = { IVA_GENERAL, conIva };
// main.cjs (CommonJS)
const { conIva } = require("./precios.cjs");
console.log(conIva(10).toFixed(2));
12.10
Módulos ES (el estándar)
export/import- Navegador y Node
- Extensión obligatoria en las rutas
CommonJS (el sistema propio de Node)
module.exports/require()- Solo Node y herramientas que lo emulan
- La extensión es opcional
El error de mezclar export y module.exports
La primera versión de este artículo tenía export function y, al final del mismo fichero, module.exports = romanNumerals. Son dos sistemas distintos, y Node no los mezcla en un fichero:
// error-mezcla.mjs
export function conIva(precio) {
return precio * 1.21;
}
module.exports = conIva;
ReferenceError: module is not defined in ES module scope
Al revés, un import en un fichero CommonJS da otro error frecuente:
SyntaxError: Cannot use import statement outside a module
La solución es poner "type": "module" en el package.json o cambiar la extensión a .mjs.
Usar los dos sistemas a la vez
Lo que sí se puede es que un fichero de un sistema importe otro del otro sistema:
// usa-cjs.mjs (módulo ES que carga CommonJS)
import precios from "./precios.cjs";
import { conIva } from "./precios.cjs";
console.log(precios.IVA_GENERAL, conIva(10).toFixed(2));
0.21 12.10
Desde un módulo ES, module.exports llega como exportación por defecto, y Node intenta además ofrecer sus propiedades como exportaciones con nombre.
// formato.mjs (módulo ES)
export const MONEDA = "EUR";
export default function formatear(cantidad) {
return `${cantidad.toFixed(2)} €`;
}
// usa-esm.cjs (CommonJS que carga un módulo ES)
const formato = require("./formato.mjs");
console.log(formato.default(12.1), formato.MONEDA);
12.10 € EUR
require() de un módulo ES funciona sin opciones desde Node 20.19.0 y 22.12.0, según la documentación de Node. Devuelve el objeto del módulo, con lo exportado por defecto en default, y no admite módulos con await fuera de una función: para esos, import().
Para código nuevo, usa export e import.
Usarlo en un test con Jest
El test del artículo original, actualizado, con la función de números romanos:
// src/romanNumerals.js
export function romanNumerals(number) {
const numerals = [
{ value: 4, numeral: "IV" },
{ value: 1, numeral: "I" },
];
let result = "";
for (const { value, numeral } of numerals) {
while (number >= value) {
result += numeral;
number -= value;
}
}
return result;
}
// test/romanNumerals.test.js
import { romanNumerals } from "../src/romanNumerals.js";
describe("romanNumerals", () => {
it("convierte números a romanos", () => {
expect(romanNumerals(1)).toBe("I");
expect(romanNumerals(3)).toBe("III");
expect(romanNumerals(4)).toBe("IV");
});
});
Con Jest 30.5.2 y "type": "module", Jest necesita que Node arranque con --experimental-vm-modules:
{
"name": "roman-jest",
"type": "module",
"scripts": { "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js" }
}
(node:2333747) ExperimentalWarning: VM Modules is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
El ExperimentalWarning lo escribe Node, no Jest: avisa de que la opción --experimental-vm-modules es experimental. No es un error y el test pasa.
Si en lugar de eso te sale Cannot use import statement outside a module, mira cómo resolver el error de import en Jest; y para montarlo desde cero, cómo instalar Jest en un proyecto de JavaScript. Si quieres llegar hasta 3999 test a test, la tienes resuelta en la kata Roman Numerals con TDD.
Fuentes
Comprobado el 9 de noviembre de 2026:
- MDN: JavaScript modules y Using modules on the web (las dos modificadas el 5 de noviembre de 2026:
file://y CORS, aplazamiento automático, una sola ejecución, import maps,import()yawaitfuera de funciones). Referencia de export e import. - Node.js, documentación de la versión 26.13.0: ECMAScript modules (cómo se activan, detección de sintaxis, extensiones obligatorias, importar CommonJS,
import.meta.dirname) y CommonJS modules (cómo decide el tipo de un.jsyrequire()de módulos ES, con su historial de versiones).
Ejecutado con Node 24.21.0 (los ejemplos de Node, los errores, npm init -y con npm 11.19.0 y el test con Jest 30.5.2) y con Google Chrome 157 sin interfaz (la página con type="module", la misma sin él y la apertura desde file://). Las salidas son las reales; en las rutas largas se han sustituido directorios por ….