JASoft.org

El blog de José Manuel Alarcón Aguín. Programación web y mucho más...

MENÚ - JASoft: JM Alarcón

Cómo obtener una referencia al ámbito global en cualquier entorno JavaScript (HTML, Node.js, Windows Scripting Host...)

El ámbito global de JavaScript es el ámbito superior en el que se definen las variables y que contiene algunas de las funcionalidades globales del entorno de ejecución.

Se trata de un objeto especial que:

  • Siempre está disponible
  • Se llama de manera implícita, es decir, no es necesario mencionar su nombre explícitamente para usar sus métodos o propiedades, al contrario que con cualquier otro objeto.
  • Según indica el estándar ECMAScript, este objeto global no dispone de constructor ni de prototipo y no se puede invocar como una función al igual que el resto de los objetos.

Si en un fragmento de código JavaScript definimos una variable o una función como estas fuera de todo contexto, o sea, para simplificar, sin ser dentro de una función):

var miVar = 0;
function sumar(a, b) {
   return a+b;
}

Lo que estamos haciendo es crear sendos miembros en el objeto global, que es el que constituye dicho ámbito: una variable y una función globales.

Es decir, declarar una variable o una función global implica crear un miembro de este objeto global.

Además, se definen otros ámbitos según en dónde declaremos cada variable, y estos ámbitos se "contienen" unos a otros. Es decir, los ámbitos van en una jerarquía que comienza en el ámbito más restringido y sube hasta llegar al ámbito global. En nuestro ejemplo híper-sencillo tendríamos tan solo dos ámbitos: el global y el de la función:

JavaScript-Ambitos

Desde el ámbito de la función tenemos acceso al ámbito global.

Todo esto de los ámbitos se complica mucho más (tenemos clausuras, por ejemplo). En el estándar podemos leer sobre ello (les llama entornos léxicos) y que nos de vueltas la cabeza.

Como sabemos, aunque nació en los navegadores, JavaScript en la actualidad se utiliza para casi todo, y podemos encontrarlo en casi cualquier entorno imaginable: servidores, dispositivos embebidos, sistemas operativos...

Aunque el estándar ECMAScript describe el objeto global y qué debe contener, no es igual en todos los entornos.

En un navegador el ámbito global lo constituye la ventana en la que se ejecuta el script, y de hecho podemos acceder a él mediante el objeto window. Por ejemplo, a partir del código anterior que definía una variable y una función podríamos escribir:

alert(miVar);
alert(window.miVar);

Y veríamos que son instrucciones equivalentes, ya que al ser una variable global queda definida dentro del ámbito global (es decir, de window en un navegador).

En otros entornos, sin embargo, no disponemos de un objeto window que actúe de ámbito global. Por ejemplo, en Node.js existe un objeto llamado global (o GLOBAL, es un alias) que nos permite acceder al ámbito global. En Windows Scripting Host el objeto global no tiene un nombre definido que lo represente y que nos permita acceder a él directamente... Cada entorno, por tanto, tiene sus particularidades.

La pregunta entonces es: ¿Cómo podemos acceder al ámbito global de JavaScript de manera genérica e independiente del entorno?.

Acceso al ámbito global

Antes de proceder vamos a escribir un par de funciones auxiliares que nos ayuden a comprobar si el objeto que obtenemos es el correcto o no. Se trata de las dos siguientes:

function echo(msg){
    if (typeof console != 'undefined' && console.log)
        console.log(msg);
    else if (typeof WScript != 'undefined') {
        WScript.echo(msg);
    }
}

function ResumeObjeto(obj) {
    var prop;
    var informe = "";

    for (prop in obj) {
        informe += ("- " + prop + ": " + typeof(obj[prop]) + "\n");
    }
    
    return informe;
}

La primera de ellas nos servirá para mostrar información textual independientemente del entorno en el que estemos. Básicamente utiliza el método WScript.echo en Windows Scripting Host, y console.log en todos los demás entornos (navegador, Node.js...). Así podemos despreocuparnos de cómo mostrar la información y simplemente llamamos a echo.

La segunda lo que hace es mostrar el nombre y el tipo de todos los miembros enumerables de un objeto JavaScript. De este modo podemos conocer todos los campos, propiedades y funciones de las que dispone un objeto y que es posible enumerar (más sobre este detalle luego).

Vale. Ahora, armados con estas dos funciones, ¿cómo obtenemos una referencia al objeto global?.

Un primer intento podría ser obtener una referencia ejecutando una función global y recogiendo el valor de this, algo así:

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

echo(ResumeObjeto(ambitoGlobal));

Es decir, auto-ejecutamos una función anónima que devuelve el valor de su contexto, que será el contexto global.

Ingenioso, pero problemático. El problema es que esto funcionará en la mayoría de los casos, pero ¿qué pasa si añadimos la línea mágica "use strict" al principio? Pues que dejará de funcionar y dejará la variable ambitoGlobal sin definir.

El motivo es que, por defecto, cuando una función se llama fuera de un contexto específico, this se asocia automáticamente al ámbito global. Pero en modo estricto esto no es así y this queda sin definir. De ahí que no funcione.

Las funciones ejecutadas sin contexto en el modo estricto no asignan el ámbito global a this.

Así que el método, aunque vale para muchos casos, no es lo suficientemente genérico y fallará en modo estricto. Necesitamos algo mejor.

Para ello usaremos otra particularidad de JavaScript y es que cuando se declara una función con el constructor de la clase Function,  nunca se definirá como estricta aunque el código desde el que se está ejecutando sea estricto. Salvo, claro está, que la propia función que definamos comience con "use strict".

Esto nos lleva a esta técnica para determinar el objeto global:

var ambitoGlobal = Function('return this')();

Que devuelve una referencia al ámbito global y que va a funcionar en todos los casos.

¡Listo! Vamos a comprobarlo en varios entornos:

1.-  Navegadores

Si la ejecutamos en un navegador veremos por la consola una lista larguísima de métodos que forman parte de la ventana (recordemos: en el navegador el ámbito global coincide con la ventana del navegador), incluyendo además las definiciones globales que hayamos hecho en nuestro código, como en nuestro caso las dos funciones auxiliares:

objGlobal-Navegador

2.- Windows Scripting Host

En este caso la clase es mucho más simple y tiene dos simples miembros además de los elementos globales que hayamos declarado nosotros mismos:

objGlobal-WSH

Puedes probarlo en cualquier versión de Windows simplemente haciendo doble-clic sobre el archivo .js de ejemplo de la descarga del final.

3.- Node.js

Si ejecutamos exactamente el mismo script con Node.js obtenemos lo siguiente por la consola:

objGlobal-NodeJS

En este caso la cosa no parece haber funcionado del todo bien. Yo personalmente le veo dos pegas al menos:

  1. No aparecen nuestros miembros globales (echo, ResumeObjeto, ambitoGlobal) que sí aparecían en los otros. De hecho sin embargo aparecen unas propiedades global y GLOBAL que supuestamente son referencias al ámbito global, WTF?.
  2. Faltan muchos de los miembros que, estando Node.js basado en V8 (el motor de JavaScript de Chrome) esperaríamos encontrar en el ámbito global, como algunas funciones propias del lenguaje (parseInt, eval...), objetos auxiliares (Math, JSON...) o los tipos fundamentales (String, Number...). Es decir, esperaría una lista no tan larga como la del navegador, pero sí mucho más larga.

¿Estamos realmente frente al ámbito global como esperábamos o es otra cosa?

La primera "pega" realmente no es un problema. No aparecen nuestras funciones definidas en el ámbito global precisamente por el modo que tiene de funcionar Node.js. Nuestro archivo .js se ejecuta como un módulo y cuando definimos algo en él no se declara en el ámbito global sino que se declara dentro de la clausura propia del módulo que se está ejecutando, es decir, dentro del ámbito del módulo. Esto es así precisamente para evitar polucionar por error el ámbito global.

En Node.js la ejecución de cada archivo se aísla en su propio ámbito de módulo para evitar conflictos por polucionarse el ámbito global.

Así que es normal que no nos aparezcan en el objeto que representa a éste. Tampoco podemos acceder a enumerarlas de ningún modo (ni siquiera con this) porque JavaScript no ofrece una forma desde código convencional de acceder a la jerarquía de ámbitos que forman parte de una clausura.

JavaScript no ofrece una manera programática de acceder a la cadena de ámbitos.

El hecho de que este objeto tenga dos propiedades global y GLOBAL se debe a que Node.js nos ofrece estos dos alias en el ámbito global como una manera de acceder al ámbito global ;-)

En cuanto a la segunda pega, efectivamente, faltan miembros en ese listado. El motivo es que todos esos miembros están marcados como no-enumerables y por ello no son accesibles mediante la sintaxis objeto[propiedad].

La forma en la que podemos acceder a ellos y verlos es recurriendo al método getOwnPropertyNames de la clase genérica Object, base de todas las demás, que devuelve una matriz con los nombres de todos los miembros del objeto aunque no sean enumerables. Como es una matriz incluso los podemos ordenar para sacarlos por orden alfabético:

echo(Object.getOwnPropertyNames(ambitoGlobal).sort());

Esto nos devuelve una lista larga con todos los verdaderos miembros del ámbito global en Node.js:

objGlobal-NodeJS-Todas

Viéndose, ahora sí, todos los elementos que cabría esperar. He dejado esa línea comentada en el archivo de ejemplo.

He dejado en un ZIP (768 bytes) el ejemplo que he utilizado. Contiene un archivo .html para probarlo en un navegador, y un archivo .js "híbrido" que puedes ejecutar directamente tanto en Windows como en Node.js (e imagino que en otros entornos JavaScript).

Es un tema bastante interesante teóricamente.

José Manuel Alarcón José Manuel Alarcón
Fundador de campusMVP.es, el proyecto de referencia en formación on-line para programadores en lengua española. Autor de varios libros y cientos de artículos. Galardonado como MVP de Microsoft desde 2004. Gallego de Vigo, amante de la ciencia y la tecnología, la música y la lectura. Ayudando a la gente en Internet desde 1996.
Descarga GRATIS mi último libro (no técnico): "Tres Monos, Diez Minutos".
Banner

Agregar comentario