Nivel inicial12 min de lectura · Publicado el

Exportar e importar funciones en JavaScript (import/export)

Exportar e importar funciones en JavaScript: lo que lleva export queda en la bandeja y main.js lo coge con import. Sin export no hay importrubentejera.com precios.jsexportmain.jsimportsin export, dentro
El módulo precios.js es una caja de herramientas. La función con export, conIva, queda en la bandeja de fuera y main.js la coge con import { conIva }; lo que no exportas se queda dentro.

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 muestra Uncaught SyntaxError: Cannot use import statement outside a module y 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 URL file:// 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 con Error [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én index.js si 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 con module, __filename y __dirname; los dos últimos se sustituyen por import.meta.filename e import.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() y await fuera 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 .js y require() 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 ….