Uso explícito del contexto global en Javascript

17 Feb 2011

Vía Jonathan Fine, leo un modelo interesante a la hora de diseñar la arquitectura de nuestras aplicaciones Javascript. El artículo es de 2009, pero gracias al auge (o reinvención) del Javascript a nivel de servidor, vuelve a cobrar interés.

El problema

Cuando diseñamos nuestras aplicaciones siempre buscamos mantener el entorno global lo más limpio posible de variables. Para esto, una buena solución es declarar una función anónima autoejecutable que englobe todo nuestro código creando un contexto (closure) propio. Es el patrón de diseño que denominamos de contexto dinámico:

var myApp = {};
( function( context ){
  var foo = 'Hello';
  var bar = 'World';
  context.sum = function( param1, param2 ){
    return param1 + param2;
  };
  context.myMessage = function(){
    return foo + ' ' + bar;
  }
} )( myApp );
 
console.log( myApp.sum( 10, 5 ) ); // 15
console.log( myApp.myMessage() ); // Hello World

Declaramos una variable a la que asignamos un ‘entorno’ para pasarla como argumento a la función autoejecutable: un buen diseño. Para más información y ejemplos, podemos ojear el artículo Namespancing en Javascript.

El problema con este patrón es que cuando necesitamos añadir algo de forma explícita al ámbito global, la cosa se vuelve poco intuitiva: ¿ usamos this ? ¿ usamos window ?

La solución

Un pequeño truco para solucionar este problema, es crear un objeto global dentro de nuestra función cuyos métodos serán siempre accesibles desde fuera:

( function(){
  var global = ( function(){ return this; } ).call();
  global.Formula = function (){
    // body of function
    return 'Hello World';
  };
} )();
 
console.log( Formula() ); // Hello World

Este código sería equivalente a:

var Formula;
( function(){
  Formula = function (){
    // body of function
    return 'Hello World';
  };
} )();
 
console.log( Formula() ); // Hello World
¿Por qué no usamos this.Formula simplemente?

Porque this solo hace referencia al objeto global cuando se declara en el nivel más alto; en estructuras muy anidadas su uso es peligroso.

¿Y por qué no utilizamos window.Formula si el objeto siempre referencia al ámbito global?

Porque window solo funciona correctamente cuando ejecutamos nuestra aplicación en el contexto de un navegador (HTML). Para arquitecturas de escritorio como por ejemplo GNOMEShell o incluso de servidor con Node.js o Rhino, podemos encontrarnos con comportamientos no deseados, de ahí el nuevo interés por este método.

Aplicando call al objeto this, nos aseguramos de que la función es ejecutada en el nivel más alto y, por tanto, referencia al ámbito global sin equívocos.

Y ahora, la versión para el ECMAScript5 Strict Mode

Una de las particularidades del ECMAScript5 strict mode es que cuando invocamos this dentro de la función anónima anterior, siempre obtendremos un valor undefined. Para solucionar este inconveniente, tenemos que echar mano de la documentación hasta encontrar una solución válida: el temido e incomprendido eval.

En la nueva especificación ECMAScript5, el método eval siempre se ejecuta en el ámbito global de la aplicación, independientemente de dónde lo invoquemos. Esto permite resolver el problema de una manera sencilla aunque poco intuitiva: realizando una llamada indirecta al método.

El ejemplo anterior quedaría ahora de la siguiente forma:

//strict-mode
( function(){
  "use strict";
  var ieval = eval; // Indirect eval call
  var global = ieval('this');
  global.Formula = function (){
    // body of function
    return 'Hello World';
  };
} )();
 
console.log( Formula() ); // Hello World
Más:

{4} Comentarios.

  1. joseanpg

    Buen artículo Carlos.

    Te dejo un par de pequeñas aportaciones. La primera: no es necesario utilizar call, es decir, en lugar de

    var global = ( function(){ return this; } ).call();

    podemos usar

    var global = ( function(){ return this; } )();

    La segunda es que en strict mode este truco no funciona.

    • Carlos Benítez

      Gracias joseanpg!
      Tienes razón en cuanto al strict-mode ya que en ese caso, this siempre devolvería undefined.

      Para solucionarlo, tendríamos que hacer una llamada directa a eval, que siempre se ejecuta en el entorno global.

      Quedaría algo así:

      var ieval = eval;
      var global = ieval('this');
      

      Saludos!

  2. joseanpg

    Carlos, hay una pequeña errata: esa es una llamada directa a eval

    Un saludo.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *