IoC reduce el acoplamiento del código JavaScript: un módulo no importa estáticamente una implementación concreta, sino que recibe los objetos necesarios desde fuera. Este es un recorrido didáctico hacia un contenedor de objetos pequeño. Se refiere a JavaScript ES6+ sin transpilación, no a decoradores TypeScript.
El objetivo no es una biblioteca universal lista para usar, sino comprender la mecánica: cargar módulos ES, crear dependencias e insertarlas en el lugar adecuado. El contenedor de demostración ocupa unas 35 líneas.
1. Composition Root
Con control directo, el servicio importa su logger:
import logger from './logger.js';
export default class Service {
exec(opts) { logger.info(JSON.stringify(opts)); }
}
Con inversión, el servicio recibe la dependencia por constructor:
export default class Service {
constructor(logger) {
this.exec = (opts) => logger.info(JSON.stringify(opts));
}
}
Alguien sigue teniendo que importar y crear los objetos. Ese lugar es el Composition Root:
import logger from './logger.js';
import Service from './service.js';
const service = new Service(logger);
service.exec({ name: 'Composition Root' });
2. Fábricas
Para simplificar la demostración, cada módulo exporta una fábrica asíncrona que recibe dependencias y produce el resultado:
export default async function factory(logger) {
return (opts) => logger.info(JSON.stringify(opts));
}
En código real un export puede ser clase, función u objeto; este acuerdo solo aclara el siguiente paso.
3. Especificación de dependencias
Los nombres de argumentos comunes pueden cambiar con minificación. Por eso se pasan dependencias en un solo objeto:
function factory({ logger, config }) { /* ... */ }
Al principio se puede usar temporalmente la ruta del módulo como clave:
export default async function factory({ ['./logger.js']: logger }) {
return (opts) => logger.info(JSON.stringify(opts));
}
Así el contenedor recibe la información que debe analizar.
4. Analizador de especificación
El ejemplo simplificado convierte una función en texto, extrae parámetros con una expresión regular y obtiene las claves. Es un recurso didáctico, no una recomendación de producción: en un contenedor real el contrato debe declararse mediante metadatos explícitos y fiables.
function parse(def) {
const params = /function\s+\w+\s*\(\s*\{([\s\S]*?)\}\s*\)/.exec(def)?.[1];
if (!params) return [];
return params.split(',').map((entry) => entry.split(':')[0]
.trim().replace(/[\[\]'\"]/g, ''));
}
5. Contenedor
El contenedor importa recursivamente módulos, encuentra dependencias, construye la especificación y guarda resultados en caché:
const cache = {};
async function get(key) {
if (cache[key]) return cache[key];
const { default: factory } = await import(key);
const spec = {};
for (const path of parse(factory.toString())) spec[path] = await get(path);
return (cache[key] = await factory(spec));
}
Ahora el Composition Root se reduce a una línea:
const service = await container.get('./service.js');
6. Resolver y enlace tardío
Las rutas en la especificación aún atan el código a detalles. El paso final deja únicamente abstracciones:
export default async function factory({ logger, config }) {
return (opts) => logger.info(`${config.appName}: ${JSON.stringify(opts)}`);
}
La correspondencia entre abstracción e implementación se mueve a un mapa de root:
container.setMap({
service: './service.js',
logger: './logger.js',
config: './config.js',
});
const service = await container.get('service');
Eso es enlace tardío: al iniciar se puede elegir un logger de consola o archivo sin reescribir el servicio. El código pasa de ser una estructura «soldada» a una «atornillada»: un módulo se separa y reutiliza con más facilidad.
En un proyecto grande nadie mantiene manualmente un mapa para cada
nombre. Las convenciones convierten Vendor_Package_Mod en una ruta
como node_modules/@vendor/package/src/Mod.js; el identificador incluso
puede incluir ciclo de vida o ámbito.
Conclusión
DI resulta especialmente útil con muchos archivos y paquetes: reduce acoplamiento y mejora pruebas. En frontend tradicional los bundlers suelen ocultar módulos individuales, pero en una PWA Service Worker puede descargar y cachear conjuntos de archivos ES. Así el mismo principio de enlace funciona en Node.js y navegador. Para producción conviene usar un contenedor maduro como @teqfw/di, no el parser didáctico del artículo.