miércoles, 23 de noviembre de 2011

Expresiones de función invocadas inmediatamente ( IIFE )

NOTA IMPORTANTE: Este artículo es una traducción semilibre de este otro artículo de Ben Alman (@cowboy) que me resultó muy interesante: http://benalman.com/news/2010/11/immediately-invoked-function-expression/. A Ben le corresponde todo el mérito de la explicación.

Otro elemento a tener en cuenta es que lo que se trata en este artículo es sobre JavaScript, independientemente de que se ejecute en cliente o en servidor.



En caso de que no os hayáis dado cuenta, soy un poco purista de la terminología. Así que, después de escuchar demasiadas veces el popular y, sin embargo, engañoso término de javascript "función anónima autoejecutable", he decidido finalmente organizar mis pensamientos en un artículo.

Además de proporcionar una información muy completa acerca de cómo funciona realmente este patrón, hago una recomendación sobre cómo deberíamos llamarlo.

Por favor, ten en cuenta que este artículo no pretende ser uno del tipo "yo tengo razón y tú estás equivocado". Estoy realmente interesado en ayudar a la gente a comprender conceptos potencialmente complejos, y creo que el uso de una terminología coherente y precisa es una de las cosas más fáciles que la gente puede hacer para facilitar la comprensión.

Entonces, en cualquier caso, ¿de qué va todo esto?
En JavaScript, cada función, cuando se invoca, crea un nuevo contexto de ejecución. Dado que las variables y funciones definidas dentro de una función sólo se pueden acceder desde su interior, pero no desde fuera, ese contexto creado por la invocación de una función proporciona una manera muy fácil de crear "privacidad".

// Debido a que esta función devuelve otra función que tiene acceso
// a la variable "privada" i, la función devuelta tiene unos ciertos "privilegios"

function crearContador() {
  // `i` solo es accesible desde dentro de `crearContador`.
  var i = 0;

  return function() {
    console.log( ++i );
  };
}

// Fíjate en que tanto `contador` como `contador2` tienen 
// cada uno su propio `i` independiente.

var contador = crearContador();
contador(); // logs: 1
contador(); // logs: 2

var contador2 = makeCounter();
contador2(); // logs: 1
contador2(); // logs: 2

i; // ReferenceError: i is not defined (sólo existe dentro de crearContador)
  
En muchos casos, no necesitarás varias "instancias" de lo que sea que devuelva la función crearLoQueSea, pudiendo hacer lo necesario con una sola instancia y en otros casos ni siquiera estarás devolviendo de manera explícita un valor.

El fondo de la cuestión
Veamos: si se define una función como 'function foo(){}' o 'var foo = function(){}' , lo que acabas teniendo es un identificador de una función, que se puede invocar al poner paréntesis () detrás, como 'foo()'.

// Debido a que una funcion solo se puede invocar poniendo () después del nombre
// como foo(), y debido a que foo es solo la referencia a la expresión de una función
// como `function() { /* código */ }`...

var foo = function(){ /* código */ }

// ... no se podría deducir que la función se podría invocar poniendo () inmediatamente
// después de la expresión de la función?

function(){ /* código */ }(); // SyntaxError: Unexpected token (
 
Como puedes ver, esto falla.

Cuando el parser se encuentra con la palabras clave 'function' en el ámbito global o dentro de una función, la considera como la declaración de una función y no como una expresión de función. Si no le indicas al parser explícitamente que debe espirar una expresión, lo interpreta como la declaración de una función sin nombre y lanza una excepción de SyntaxError porque las declaraciones precisan siempre de un nombre.

Una digresión: las funciones, paréntesis, y SyntaxErrors
Curiosamente, si especificas el nombre de una función y pones los paréntesis inmediatamente después de la función, el parser también lanzaría un SyntaxError, pero por una razón diferente.

Mientras que los paréntesis colocados después de una expresión indican que la expresión es una función que se debe invocar, los paréntesis colocados después de una declaración se interpretan como totalmente separados de la declaración y actúan como un operador de agrupación (utilizado como un medio para controlar la precedencia de evaluación).

// Aunque la declaración de la función es sintácticamente válida, es una declaración
// y los parántesis que hay a continuación resultan erróneos porque como operador 
// de agrupación necesitan contener una expresión

function foo(){ /* código */ }(); // SyntaxError: Unexpected token )

// Ahora bien, si introduces una expresión dentro de los paréntesis
// no se lanza ninguna excepción...
// pero la función tampoco se ejecuta, porque esto:

function foo(){ /* código */ }( 1 );

// Es equivalente a lo siguiente, una declaración de función seguida de una 
// expresión completamente independiente.

function foo(){ /* código */ }

( 1 );
  
Expresión de función invocada inmediatamente o Immediately-Invoked Function Expression, IIFE
Afortunadamente, corregir SyntaxError es sencillo. La forma más aceptada para informar al parser que debe esperar una expresión de función es envolverla en paréntesis, porque en JavaScript, los paréntesis no pueden contener declaraciones. En este punto, cuando el parser encuentra la palabra clave 'function' sabe que debe analizarlo como una expresión de función y no una declaración de función.

// Cualquiera de los dos siguientes patrones se pueden usar para invocar
// de forma inmediata una expresión de función, utilizando el contexto 
// de ejecución de la función para crear "privacidad"

(function(){ /* código */ }()); // Crockford recomienda esta
(function(){ /* código */ })(); // Pero esta funciona igual de bien

// Puesto que el punto de las paréntesis o los operadores es forzar
// a desambiguar entre expresiones de funciones y declaraciones de funciones,
// se pueden evitar cuando el parser exprea una expresión (ver la nota más abajo)

var i = function(){ return 10; }();
true && function(){ /* código */ }();
0, function(){ /* código */ }();

// Si no te interesa el valor devuelto, o el hecho de que tu código 
// resulte un poco más ilegible, puedes ahorrarte algún byte prefijando
// la función con un operador unario

!function(){ /* código */ }();
~function(){ /* código */ }();
-function(){ /* código */ }();
+function(){ /* código */ }();

// Esta es otra variación  de @kuvos - No tengo clara las implicaciones
// en rendimiento, si las hubiera, por utilizar la palabra clave 'new', pero funciona.
// http://twitter.com/kuvos/status/18209252090847232

new function(){ /* código */ }
new function(){ /* código */ }() // Solo se necesita paréntesis si se pasan argumentos
  
Una nota importante sobre los paréntesis 
En los casos en los que los paréntesis extra de "desambiguación" que rodean la expresión de la función sean innecesarios (porque el parser ya espera una expresión), sigue siendo una buena idea usarlos cuando se realiza una asignación, por convención.

Dichos paréntesis suelen indicar que la expresión de la función se invocará de inmediato, y que la variable contendrá el resultado de la función, no la propia función. Esto puede ahorrarle a alguien que esté leyendo el código, la molestia de tener que desplazarse hasta el final de lo que podría ser una expresión de función muy larga para ver si finalmente se ha invocado o no.

Como regla general, aunque escribir código ambiguo podría ser técnicamente necesario para mantener el parser JavaScript de lanzar excepciones SyntaxError, escribir código sin ambigüedades es también es bastante bueno para evitar que otros desarrolladores te lanzan a tí excepciones del tipo "WTFError"!

Guardando el estado con closures
Al igual se pueden pasar argumentos a las funciones cuando se invocan mediante su identificador, también se pueden pasar cuando se invoca inmediatamente una expresión de función. Puesto que la función (el closure) referencia los valores pasados, estos quedan "fijados" de modo que una Immediately-Invoked Function Expression se puede utilizar para guardar eficazmente un estado.

Si deseas obtener más información sobre los 'closures', puedes consultar este artículo: http://skilldrick.co.uk/2011/04/closures-explained-with-javascript/

// Esto no funciona como podrías pensar en primera instancia,
// porque el valor de 'i' nunca queda fijado. Lo que ocurre es que
// cuando haces click en cada enlace (después de que se haya ejecutado
// el bucle), el alert informa del total de elementos porque es el valor
// que tiene la variable 'i' en ese punto

var elems = document.getElementsByTagName( 'a' );

for ( var i = 0; i < elems.length; i++ ) {

  elems[ i ].addEventListener( 'click', function(e){
    e.preventDefault();
    alert( ' Soy el link nº ' + i );
  }, 'false' );

}

// Esto SI funciona, porque dentro de cada closure IIFE, el valor de 'i'
// queda fijado como 'indiceFijado'. Tras ejecutarse el bucle, aunque
// el valor de 'i' es el del total de elementos, dentro de la IIFE
// el valor de 'indiceFijado' es el que se le pasase en cuando la expresión
// de función fue ejecutada, con lo que cuando un enlace se hace click
// el valor correcto aparece en el alert

var elems = document.getElementsByTagName( 'a' );

for ( var i = 0; i < elems.length; i++ ) {

  (function( indiceFijado ){

    elems[ i ].addEventListener( 'click', function(e){
      e.preventDefault();
      alert( ' Soy el link nº ' + indiceFijado);
    }, 'false' );

  })( i );

}

// Puedes usar un IIFE como este donde el closure afecta solo a la función
// handler del click y no a la asignación completa del 'addEventListener'.
// En ambos ejemplos la variable queda fijada, pero encuentro el anterior 
// más sencillo de leer

var elems = document.getElementsByTagName( 'a' );

for ( var i = 0; i < elems.length; i++ ) {

  elems[ i ].addEventListener( 'click', (function( lockedInIndex ){
    return function(e){
      e.preventDefault();
      alert( 'Soy el link nº' + lockedInIndex );
    };
  })( i ), 'false' );

}

// En el siguiente ejemplo se combina la variable fijada con el valor final
// de 'i'

var elems = document.getElementsByTagName( 'a' );

for ( var i = 0; i < elems.length; i++ ) {

  (function( indiceFijado ){

    elems[ i ].addEventListener( 'click', function(e){
      e.preventDefault();
      alert( ' Soy el link nº ' + indiceFijado + ' de ' + i);
    }, 'false' );

  })( i );

}
 
Tenga en cuenta que en los dos ejemplos del medio, 'indiceFijado' podría haber sido llamado 'i' sin ningún problema, pero el uso de un identificador diferente, como un argumento de la función hace que el concepto sea mucho más fácil de explicar/entender.

Uno de los efectos secundarios más ventajosos de las expresiones de función invocadas inmediatamente es que, la expresión de la función anónima se invoca de inmediato, sin necesidad de utilizar un identificador, y por tanto el closure puede ser utilizado sin contaminar el ámbito actual.

¿Qué hay de malo en la "función anónima autoejecutable"?
Ya has visto lo mencionado un par de veces, pero en el caso de que no quede claro, yo estoy proponiendo el término "expresión de función inmediatamente ejecutada", y "IIFE" (del inglés “Immediately-Invoked Function Expression”) si te gusta las siglas.

¿Qué es una IIFE? Es una expresión de función que se invoca de inmediato. Como dice su nombre, vamos.
Me gustaría ver a los miembros de la comunidad JavaScript adoptar el término "expresión de función inmediatamente ejecutada" y "IIFE" en sus artículos y sus presentaciones, porque me parece que hace que la comprensión de este concepto un poco más fácil, y porque el término "función anónima autoejecutable" no es nada preciso:

// Esto si es una función "autoejectuable"  Es una función que se ejecuta (o invoca)
// a si misma de forma recursiva:

function foo() { foo(); }

// Esta es una función anónima autoejectable. Debido a que no tiene 
// identificador, debe usar la propiedad `arguments.callee` (que
// especifica la función que se ejecuta actualmente) para ejecutarse a si misma.

var foo = function() { arguments.callee(); };

// Esto "podría" ser una función anónima autoejecutable, pero solo mientras
// el idenfificador 'foo' la referencia. Si cambiases 'foo' a otra cosa tendrías
// una función anónima "que solía auto ejecutarse"

var foo = function() { foo(); };

// Mucha gente llama esto "función anónima autoejecutable" aunque
// no se autoejecuta, porque no se invoca a si misma. Sin embargo,
// sí se invoca inmediatamente.

(function(){ /* código */ }());

// Agregando un identificador a una la expresión de una función (y creando
// por tanto una función de expresión con nombre) puede ser muy util
// en depuración. Sin embargo, una vez nombrada, la función deja de ser
// anónima

(function foo(){ /* code */ }());

// Las IIFEs también pueden ser autoejecutables, aunque probablemente este
// no sea el patrón más útil

(function(){ arguments.callee(); }());
(function foo(){ foo(); }());

// Un último apunte: esto causará un error en BlackBerry 5, debido a que
// dentro de la función con nombre, ese nombre es "undefined". Awesome, huh?

(function foo(){ foo(); }());
  
Espero que estos ejemplos hayan dejado claro por qué el término "autoejecutable" es algo engañoso, ya que la función no se ejecuta a sí misma, a pesar de que la función está siendo ejecutada. Además, "anónimo" es innecesariamente específico, ya que una expresión de función invocada inmediatamente puede ser tanto anónima como con nombre.

Por lo tanto, eso es todo. Esa es mi gran idea.

Dato curioso: puesto que 'arguments.callee' está en desuso en ECMAScript 5 en modo estricto, en realidad es técnicamente imposible crear una "auto-ejecución de la función anónima" en ECMAScript 5 modo estricto.

Una última acotación al margen: El patrón de módulo
Ya que he comentado las expresiones de función, sería negligente si por lo menos no mencionase el patrón de módulo. Si no estás familiarizado con el patrón de módulo en JavaScript, verás que es similar al primer ejemplo, pero devolviendo un objeto en lugar de una función (y que se implementa generalmente como un singleton, como en el siguiente ejemplo).

// Crea una función anónima que se invoca inmediatamente 
// y asigna un valor a una variable. Esto elimina el middleman
// de la función "creaLoQueSea".
// 
// Como se explicó más arriba, aunque los paréntesis no son necesarios
// en esta expresión de función, se usan como convención para 
// ayudar a clarificar que la variable se asigna con el 
// *resultado* de evaluar la función y no con la función misma.

var contador = (function(){
  var i = 0;

  return {
    get: function(){
      return i;
    },
    set: function( val ){
      i = val;
    },
    incrementa: function() {
      return ++i;
    }
  };
  
}());
 
Este enfoque del patrón de módulo no sólo es increíblemente poderoso, pero además es increíblemente sencillo. Con muy poco código, puedes crear espacios de nombre para propiedades y métodos relacionados, organizando módulos de código enteros de una manera que minimice la contaminación del ámbito global y cree privacidad.

Seguir leyendo
Espero que este artículo fuese ilustrativo y que haya respondido a algunas de tus preguntas. Por supuesto, si tienes más dudas que cuando empezaste a leer, te listo unos artículos (en inglés) con los que puedes ampliar tus conocimientos:


Me gustaría dar las gracias a Asen Bozhilov y John David Dalton por contribuir con su asesoramiento técnico, así como a Morgan Nick por sus puntos de vista.


3 comentarios:

  1. Me lo comentaste el otro día pero con estos ejemplos ya lo termine de comprender. Decididamente me esta picando la curiosidad por js. Tiene. Muchísimo potencial y Java se esta ole do demasiado despacio.

    ResponderEliminar
  2. Yo estoy haciendo pruebas... y me gusta mucho. Sobre todo, me esta encantando coffeescript. Es un lenguaje con una sintaxis mucho mas clara y que compila js.
    Una especie de sass para css.

    ResponderEliminar
  3. yo solo quiero que esto me devuelva el valor y no lo logro.

    anda bien, pero pierdo el valor cuando la llamo desde por ejemplo: a = getLoad();

    function getLoad() {

    fs.readFile(connectionStringName, 'utf8', function (err, data) {

    if (err) {

    /* SEND error a syslog */
    return err;
    }
    else {

    /* SEND error a syslog */
    return JSON.parse(data);
    }
    });

    };

    ResponderEliminar