IIFE, módulos y namespaces en JavaScript

En esta página
Enterate del próximo post
Un mail cuando publico algo nuevo. Sin spam y te podés dar de baja cuando quieras.

Un mail cuando publico algo nuevo. Sin spam y te podés dar de baja cuando quieras.
El post anterior sobre objetos cerró con una promesa: agrupar funciones relacionadas dentro de un mismo objeto para simular un espacio de nombres, con una técnica que además permite mantener parte de ese contenido privado. Antes de llegar a esa técnica conviene ver primero el problema que resuelve: qué pasa cuando dos partes distintas de un mismo programa declaran algo con el mismo nombre, en el mismo scope.
var version = "1.0";
function mostrarVersion() {
console.log(`Versión: ${version}`);
}
var version = "2.0";
mostrarVersion();
// Versión: 2.0mostrarVersion se define justo después de la primera línea, con version en "1.0". Pero entre esa definición y el momento en que se la llama, otra línea del mismo archivo vuelve a declarar version, esta vez con "2.0". var permite volver a declarar la misma variable sin ningún error (a diferencia de let o const, que ya tiraban un error al intentar algo parecido), así que la segunda declaración simplemente pisa el valor de la primera. Cuando mostrarVersion finalmente se ejecuta, lee el valor que hay en version en ese momento, que ya es "2.0".
En un archivo tan corto el problema salta a la vista. En un programa real, con ese mismo nombre declarado en dos partes separadas (dos archivos cargados uno después del otro, o dos secciones de un mismo script escritas por personas distintas), una puede pisar silenciosamente a la otra sin que nada avise que pasó.
Una forma de evitar esa colisión es no dejar ningún nombre expuesto al scope donde ocurre el problema. Una función que se define y se llama en el mismo momento, sin quedar guardada en ninguna variable, es una IIFE (function expression invocada inmediatamente).
(function () {
var version = "1.0";
console.log(`Versión: ${version}`);
})();
// Versión: 1.0El primer par de paréntesis envuelve la función completa, lo que la convierte en una expresión de función en vez de una declaración (la misma distinción que ya se vio con function como sentencia o como expresión). El segundo par de paréntesis, pegado justo después, la llama en el acto. La función corre una sola vez, en esa línea, y no queda ningún nombre de función colgando en el scope que la rodea para volver a llamarla después.
(function () {
var version = "1.0";
})();
console.log(version);
// ReferenceError: version is not definedversion se declaró adentro de la IIFE, así que vive en el scope de esa función (la misma regla de scope de función que ya se vio antes), y deja de existir en cuanto la función termina de correr. Pedirla desde afuera tira el mismo ReferenceError de siempre: no hay ninguna variable con ese nombre en un scope al que se pueda llegar desde ahí. El problema de la sección anterior queda resuelto de raíz, porque ya no hay ningún nombre compartido que pisar.
Una función sola que se ejecuta y desaparece sirve para encerrar código que corre una vez. Pero muchas veces lo que hace falta es agrupar varias funciones relacionadas bajo un único nombre, en vez de exponer cada una por separado.
var Matematica = {
sumar: function (a, b) {
return a + b;
},
restar: function (a, b) {
return a - b;
},
};
console.log(Matematica.sumar(4, 3));
// 7
console.log(Matematica.restar(4, 3));
// 1Matematica es un objeto común, con dos propiedades cuyos valores son funciones en vez de números o strings (una función es un valor como cualquier otro, algo que ya quedó establecido al pasarlas como argumento o devolverlas). En vez de dos nombres sueltos en el scope donde se declara (sumar y restar, cada uno con riesgo de colisión), queda un único nombre, Matematica, con las dos funciones accesibles adentro por punto. Este agrupamiento en sí ya se llama namespace: un espacio de nombres propio, separado del resto.
Un namespace como Matematica agrupa nombres, pero no protege nada: cualquier código puede sobrescribir Matematica.sumar con otra cosa. Combinar el namespace con una IIFE agrega lo que faltaba: una variable que solo las funciones del propio namespace pueden tocar.
var Version = (function () {
var numero = "1.0";
function mostrar() {
console.log(`Versión: ${numero}`);
}
function actualizar(nueva) {
numero = nueva;
}
return { mostrar: mostrar, actualizar: actualizar };
})();
Version.mostrar();
// Versión: 1.0
Version.actualizar("2.0");
Version.mostrar();
// Versión: 2.0La IIFE se ejecuta una sola vez y devuelve un objeto con dos funciones, mostrar y actualizar, que quedan guardadas en Version. numero se declaró adentro de la IIFE, así que vive en ese scope, pero no queda perdida como en el ejemplo anterior: mostrar y actualizar la conservan, cada una, porque ambas se definieron en el mismo scope donde numero existe. Llamar a Version.actualizar("2.0") cambia esa variable, y la próxima llamada a Version.mostrar() ya lee el valor nuevo.
console.log(Version.numero);
// undefinedVersion nunca devolvió una propiedad numero: solo devolvió mostrar y actualizar. Pedir Version.numero desde afuera no tira ningún error (es la misma respuesta que ya daba pedir cualquier propiedad inexistente de un objeto), simplemente no hay nada ahí. La única forma de leer o cambiar ese valor es a través de las dos funciones que sí quedaron expuestas. Eso es el patrón módulo: un namespace (Version) con una parte de su estado deliberadamente fuera de alcance, más una superficie pública explícita para tocar lo que hace falta tocar.
Cuando dos partes de un programa comparten un nombre en el mismo scope, una puede pisar a la otra sin ningún aviso. Una IIFE evita eso al ejecutar una función una sola vez sin dejar su contenido expuesto. Combinada con un objeto devuelto como namespace, agrupa funciones relacionadas bajo un único nombre; combinada además con variables declaradas en su propio scope, ese namespace puede exponer solo lo necesario y mantener el resto privado. Es el mismo mecanismo que ya se vio con los closures, aplicado a organizar código en vez de a contar cosas.
El próximo post de la serie retoma los tipos primitivos de JavaScript: los siete valores que el lenguaje no trata como objetos, y una particularidad de cómo el propio lenguaje los identifica.