Módulos — import y export

Verás export, que hace públicos los valores de un archivo, e import, que los carga: exports con nombre y por defecto, type="module" y ESM frente a CommonJS en Node.js.

Si sigues agregando el código de una pantalla de pedidos a un solo archivo, el cálculo de impuestos, el carrito y el cobro terminan mezclados en cientos de líneas, y encontrar una función lleva tiempo. Aunque dividas el código en archivos y pongas varias etiquetas <script> (la etiqueta que carga un archivo JavaScript) en el HTML, el código que usa una función no tiene forma de saber en qué archivo está.

En este artículo verás export e import, que pasan valores de un archivo a otro.

En la consola no puedes usar import

La consola de ejercicios ejecuta tu código envuelto en una función. import y export solo pueden escribirse en el nivel superior de un archivo, así que ahí dan un SyntaxError. El código de este artículo está pensado para ejecutarse en Node.js o en el navegador como archivos separados, y los resultados aparecen en los comentarios al final de cada línea.

Compartir funciones entre archivos — exports con nombre

Supón que cart-page.js (la pantalla del carrito) y checkout.js (la pantalla de pago) necesitan el mismo cálculo de impuestos. Si copias la función en los dos archivos, un cambio en la tasa de impuesto te obliga a corregir dos lugares, y si se te olvida uno, cada pantalla muestra un importe distinto.

Las variables y funciones declaradas en un módulo (un archivo JavaScript que pasa valores con import y export; cómo se carga lo verás en una sección posterior) no son visibles desde otros archivos. Si pones export delante de una declaración, la haces pública; esto se llama export con nombre. El archivo que la usa escribe el mismo nombre entre llaves: import { withTax } from "./price.js";.

// ---- price.js ----
const TAX_RATE = 0.1;                              // Sin export: solo se usa dentro de price.js

export function withTax(price) {                   // Con export: otros archivos pueden importarla
  return Math.floor(price * (1 + TAX_RATE));
}
export function formatYen(price) {
  return `${price} yenes`;
}

// ---- cart-page.js ----
import { withTax, formatYen } from "./price.js";   // Escribe entre llaves los nombres que vas a usar
console.log(formatYen(withTax(4800)));             // 5280 yenes

// Si en el import escribes un nombre que no se exporta
// import { TAX_RATE } from "./price.js";
// SyntaxError: The requested module './price.js' does not provide an export named 'TAX_RATE'
Nombres que puedes importar y que no
import { withTax,formatYen }price.jslos exportaSe carganlas dos funcionesMuestra5280 yenesimport{ TAX_RATE }TAX_RATEno se exportaDa unSyntaxErrorNo se ejecutaninguna línea decart-page.js
Los dos nombres de la fila de arriba se exportan, pero TAX_RATE, en la fila de abajo, no. Solo puedes importar los nombres que se exportan.

TAX_RATE solo la lee withTax, dentro de price.js, así que cart-page.js obtiene el precio con impuestos sin conocer la tasa. Si la tasa cambia, solo corriges la línea de TAX_RATE en price.js, y todos los archivos que importan withTax reciben el cambio.

Hacer pública una sola clase — exports por defecto

Si cart.js contiene la clase del carrito, lo principal que exporta es un único Cart. En el código de otras personas verás a menudo import Cart from "./cart.js";, pero si le agregas llaves como en la sección anterior, obtienes un SyntaxError.

Un export por defecto (como máximo uno por archivo; el archivo que lo importa no necesita usar su mismo nombre) se escribe como export default class Cart { ... }. El archivo que lo importa lo escribe sin llaves y puede usar el nombre que quiera en lugar de Cart.

// ---- cart.js ----
import { withTax } from "./price.js";
export const MAX_ITEMS = 20;                       // En el mismo archivo también puede haber exports con nombre
export default class Cart {                        // Un solo export por defecto por archivo
  #prices = [];
  add(price) { this.#prices.push(price); }
  get total() { return withTax(this.#prices.reduce((sum, price) => sum + price, 0)); }
}

// ---- checkout.js ----
import Cart, { MAX_ITEMS } from "./cart.js";       // El default va fuera de las llaves; los que tienen nombre, dentro
import ShoppingCart from "./cart.js";              // Un default se puede importar con otro nombre
const cart = new ShoppingCart();
cart.add(1200); cart.add(3600);
console.log(cart.total, MAX_ITEMS);                // 5280 20
console.log(Cart === ShoppingCart);                // true (las dos son la misma clase)

// Con llaves, busca un export con nombre llamado Cart
// import { Cart } from "./cart.js";
// SyntaxError: The requested module './cart.js' does not provide an export named 'Cart'
Las llaves deciden qué export se busca
import Cartfrom "./cart.js"Sin llaves:busca el defaultHay un exportdefault class CartPuedes usarnew Cart()import { Cart }from "./cart.js"Con llaves: buscaun export connombreNo hay export connombre CartDa unSyntaxError
La fila de abajo busca un export con nombre llamado Cart y falla. Un import sin llaves recibe el export por defecto, no uno con nombre.

El 'Cart' del final del mensaje de error es el nombre del export con nombre que buscó y no encontró. El mensaje no dice que existe un Cart por defecto, así que revisa si usaste llaves. La tabla de abajo resume las formas de import de este artículo.

Forma del importQué recibesNombre al importar
import { withTax } from …Export con nombreEl mismo del export
import Cart from …Export por defectoEl que quieras
import Cart, { MAX_ITEMS } from …Por defecto y con nombreCart y MAX_ITEMS (default primero)

Que el navegador siga los imports — type="module"

Ahora el HTML de la página de pago tiene que cargar checkout.js. Con un simple <script src="./checkout.js"></script>, Chrome muestra SyntaxError: Cannot use import statement outside a module en el import de la línea 1, y no se ejecuta nada del archivo.

Con type="module" (un atributo de <script> que ejecuta el archivo cargado como módulo), el navegador sigue los imports del archivo de entrada y descarga también los demás archivos. Estas conexiones entre imports se llaman grafo de dependencias (qué archivos importan a cuáles).

// ---- index.html: solo se indica el archivo de entrada, checkout.js ----
// <script type="module" src="./checkout.js"></script>

// ---- price.js ----
console.log("Se ejecuta price.js");                // Se muestra una sola vez, aunque lo importan 2 archivos
const TAX_RATE = 0.1;
export function withTax(price) { return Math.floor(price * (1 + TAX_RATE)); }

// ---- cart.js ----
import { withTax } from "./price.js";
console.log("Se ejecuta cart.js");
export const cartTotal = withTax(4800);

// ---- checkout.js ----
import { cartTotal } from "./cart.js";
import { withTax } from "./price.js";              // Carga el mismo price.js que cart.js
console.log(`A pagar: ${cartTotal + withTax(500)} yenes`);

// Orden en la consola: Se ejecuta price.js → Se ejecuta cart.js → A pagar: 5830 yenes
El grafo de dependencias que parte de checkout.js
index.htmltype="module"checkout.js③ al finalcart.js② despuésprice.js① primero, 1 vez
Salvo la que sale de index.html, las flechas indican la dirección de cada import. La ejecución empieza por price.js, que se ejecuta una sola vez aunque le lleguen dos flechas.

Un módulo ejecuta los archivos que importa antes de ejecutarse a sí mismo. El archivo de entrada, checkout.js, espera a que terminen los archivos que importa (cart.js y price.js), así que se ejecuta al final. Como el orden lo deciden los imports, no tienes que pensar en qué orden van los archivos en el HTML.

No funciona en un HTML abierto con file://

Si haces doble clic en un archivo HTML y se abre como file://, Chrome considera que incluso los archivos de la misma carpeta vienen de otro origen (el lugar desde el que se obtiene un archivo), y CORS (el mecanismo que restringe las cargas desde otros orígenes) bloquea la carga con type="module". Levanta un servidor web, por ejemplo con la extensión Live Server de VS Code, y abre la página desde ahí.

Distinguir los formatos en Node.js — ESM y CommonJS

En el código escrito para Node.js te vas a encontrar archivos que usan const { reserve } = require("./stock"); en lugar de import. Si copias una línea así en un archivo que usa import, se lanza un ReferenceError en tiempo de ejecución.

Además de ESM (abreviatura de ES Modules, el formato estándar que usa import y export), existe CommonJS (un formato propio de Node.js que carga con require y hace públicos los valores con module.exports). Node.js decide qué formato usa un archivo según su extensión y el campo "type" de package.json (el archivo de configuración del proyecto).

// ---- shop/package.json (el archivo con la configuración de un proyecto de Node.js) ----
// { "type": "module" }

// ---- shop/stock.cjs: su extensión es .cjs, así que se carga como CommonJS ----
function reserve(count) { return `Reservados: ${count}`; }
module.exports = { reserve: reserve, LIMIT: 3 };   // Pon todos los valores públicos en un objeto

// ---- shop/report.cjs: un archivo CommonJS carga con require ----
const { reserve, LIMIT } = require("./stock.cjs");
console.log(reserve(2), LIMIT);                    // Reservados: 2 3

// ---- shop/checkout.js: hay "type": "module", así que se carga como ESM ----
import stock from "./stock.cjs";                   // Sin llaves: recibe el valor de module.exports
console.log(stock.reserve(1));                     // Reservados: 1
const { LIMIT } = require("./stock.cjs");
// ReferenceError: require is not defined in ES module scope, you can use import instead
Alcance de "type": "module"
Carpeta shop — "type": "module" en package.json
  • Los archivos .js se cargan como ESM
checkout.js — ESM
  • Puede cargarlo con import stock from "./stock.cjs"
  • Si escribes require, se lanza un ReferenceError
Extensión .cjs — CommonJS
  • stock.cjs — hace públicos los valores con module.exports
  • report.cjs — puede cargar con require
"type": "module" convierte en ESM los archivos .js de la carpeta. Un archivo .cjs sigue siendo CommonJS, aunque esté en la misma carpeta.

import stock no lleva llaves, así que funciona igual que importar un export por defecto: recibe el objeto de module.exports. No mezcles import y require en un mismo archivo; usa uno u otro. La tabla de abajo muestra cómo se escribe cada formato y las reglas que deciden el formato de un archivo.

AspectoESMCommonJS
Cargarimport { reserve } from "./stock.js"const { reserve } = require("./stock.js")
Hacer públicoexport function reserve() { ... }module.exports = { reserve: reserve }
ExtensiónObligatoria en Node.js y navegadores ("./stock" no se encuentra)Opcional ("./stock" encuentra stock.js)
Archivos en este formato.mjs, y .js con "type": "module".cjs, y .js con "commonjs" o sin "type" (sin "type", un .js con import se vuelve a leer como ESM)
NavegadorSe carga con type="module"No se admite tal cual
QUIZ

Verificación de conocimientos

Responde cada pregunta una a una.

Pregunta 1¿Qué pasa si cargas el export por defecto Cart con import { Cart } from "./cart.js"?

Pregunta 2price.js se importa desde dos lugares. ¿Cuántas veces se muestra el console.log de su primera línea?

Pregunta 3¿Qué pasa si llamas a require en checkout.js con "type": "module"?