Fundamentos de JavaScript: Alcance, Hoisting y Contexto de Ejecución

Dominar los conceptos fundamentales de JavaScript es esencial para cualquier desarrollador. A continuación, exploramos algunos pilares clave como la elevación de variables, el ciclo de vida de la ejecución, los tipos de ámbito y el comportamiento de la palabra clave this.

Elevación de Variables (Hoisting)

En JavaScript, una declaración de variable o función se compone de dos partes: la declaración y la asignación. Por ejemplo, la línea var miNombre = 'Ejemplo'; se puede visualizar como:

var miNombre; // Parte de declaración
miNombre = 'Ejemplo'; // Parte de asignación

El "hoisting" (elevación) es un comportamiento del motor de JavaScript donde las declaraciones de variables (con var) y funciones son movidas conceptualmente al inicio de su ámbito antes de la ejecución del código. Cuando una variable es elevada, se le asigna un valor predeterminado de undefined.

Considera el siguiente fragmento de código:

console.log(miVariable); // Salida: undefined
var miVariable = "Hola Mundo";
console.log(miVariable); // Salida: "Hola Mundo"

// Internamente, durante la fase de compilación, el motor de JS lo interpreta como:
// var miVariable; // Declaración elevada
// console.log(miVariable);
// miVariable = "Hola Mundo"; // Asignación en su lugar original
// console.log(miVariable);

Es importante destacar que las declaraciones de función tienen prioridad sobre las declaraciones de variable. En caso de múltiples declaraciones con el mismo nombre, la última declaración de función prevalecerá sobre las anteriores y sobre cualquier declaración de variable con el mismo nombre. Es una buena práctica evitar las declaraciones globales sin var, let o const, ya que crean propiedades en el objeto global (window en navegadores) y pueden generar confusiones.

Proceso de Ejecución de JavaScript

El código JavaScript sigue un proceso de dos fases:

  1. Fase de Compilación: Antes de que el código se ejecute, el motor de JavaScript lo compila. Durante esta fase, se identifican las declaraciones de variables y funciones, y se almacenan en el entorno de variables del contexto de ejecución. Las variables declaradas con var reciben un valor inicial de undefined. Si hay declaraciones de funciones con nombres duplicados, la última definición sobrescribe a las anteriores.
  2. Fase de Ejecución: Una vez compilado, el motor ejecuta el código línea por línea. En esta fase, busca los valores de las variables y ejecuta las funciones definidas, interactuando con el entorno de variables establecido en la fase anterior.

Pila de Llamadas y Desbordamiento

El motor de JavaScript crea un contexto de ejecución para diferentes tipos de código. Principalmente, existen tres tipos de contextos:

  • Contexto de Ejecución Global: Se crea cuando el código global se inicia, y solo hay uno por página.
  • Contexto de Ejecución de Función: Se crea cada vez que se invoca una función. Generalmente, se destruye una vez que la función ha terminado su ejecución.
  • Contexto de Ejecución eval: Se crea cuando se usa la función eval().

Cuando se invoca una función, JavaScript crea un nuevo contexto de ejecución y lo añade a la "pila de llamadas" (call stack). El motor ejecuta el código de la función en la parte superier de la pila. Si esa función llama a otra, se crea un nuevo contexto para la segunda función y se apila encima de la primera. Cuando una función termina, su contexto se saca de la pila.

Un "desbordamiento de pila" (stack overflow) ocurre cuando la pila de llamadas se llena, normalmente por recursión infinita o anidamiento excesivo de llamadas a funciones que no terminan o liberen sus contextos.

Veamos un ejemplo de cómo los contextos se apilan y desapilan:

let baseValue = 5;

function multiply(x, y) {
 // Contexto de 'multiply' se apila
 return x * y;
}

function calculateTotal(val1, val2) {
 // Contexto de 'calculateTotal' se apila
 let multiplier = 2;
 let partialResult = multiply(val1, val2); // Invoca 'multiply', que se apila
 return baseValue + partialResult * multiplier;
}

const finalResult = calculateTotal(4, 3); // Invoca 'calculateTotal'
console.log(finalResult); // Salida: 29

En este ejemplo, calculateTotal se apila primero, luego multiply se apila. multiply termina, se desapila, luego calculateTotal termina y se desapila. Si se llamara a calculateTotal recursivamente sin una condición de salida, podría llevar a un desbordamiento de pila.

let y const: Ámbito de Bloque

El comportamiento de "hoisting" con var a menudo conduce a resultados inesperados y es considerado una deficiencia de diseño en versiones anteriores de JavaScript. Antes de ES6, JavaScript solo tenía dos tipos de ámbito: global y de función.

ES6 introdujo las palabras clave let y const, que ofrecen un "ámbito de bloque". Esto significa que las variables declaradas con let y const solo son accesibles dentro del bloque de código (definido por llaves {}) en el que fueron declaradas. A diferencia de var, estas variables no son elevadas de la misma manera; existe una "Zona Muerta Temporal" (Temporal Dead Zone - TDZ) donde la variable no es accesible antes de su declaración.

function processData() {
 var x = 10;
 if (true) {
   var x = 20; // Re-declara la misma variable 'x' de la función
   console.log(x); // Salida: 20
 }
 console.log(x); // Salida: 20 (el 'var' dentro del if afectó al 'x' de la función)

 let y = 10;
 if (true) {
   let y = 20; // Declara una nueva 'y' de ámbito de bloque
   console.log(y); // Salida: 20
 }
 console.log(y); // Salida: 10 (la 'y' del bloque no afectó a la 'y' externa)
}
processData();

Las variables const son similares a let en cuanto a ámbito de bloque, pero además son de solo lectura, lo que significa que no se pueden reasignar después de su inicialización.

Cadena de Ámbitos (Scope Chain) y Clausuras

Cada contexto de ejecución, especialmente los de función, contiene una referencia a su entorno externo, conocido como outer. Esta referencia forma la "cadena de ámbitos". Cuando el motor de JavaScript necesita encontrar el valor de una variable, primero busca en el ámbito actual. Si no la encuentra, sigue la cadena de ámbitos hacia arriba (a través de la referencia outer) hasta que encuentra la variable o llega al ámbito global.

El "ámbito léxico" (lexical scope) es fundamental aquí: el ámbito de una función se determina por el lugar donde fue declarada en el código fuente, no por cómo o dónde fue invocada.

Las "clausuras" (closures) son un concepto poderoso en JavaScript. Una clausura ocurre cuando una función interna recuerda y tiene acceso a las variables de su función externa (o "padre"), incluso después de que la función externa ha terminado de ejecutarse. Esto se debe a que las variables de la función externa siguen siendo referenciadas por la función interna en la cadena de ámbitos.

function crearContador() {
 let count = 0; // Esta variable 'count' se "cierra" en la función interna
 return function() {
   count++;
   console.log(count);
 };
}

const miContador = crearContador();
miContador(); // Salida: 1
miContador(); // Salida: 2

const otroContador = crearContador();
otroContador(); // Salida: 1 (tiene su propio 'count' independiente)

Las clausuras pueden mantener variables en memoria, lo que es útil pero también puede llevar a un consumo excesivo si no se gestionan adecuadamente (ej., si una clausura grande se mantiene viva innecesariamente). Si una clausura ya no es necesaria, es buena práctica romper la referencia a ella para permitir que el recolector de basura de JavaScript libere la memoria.

La palabra clave this

Es crucial entender que la cadena de ámbitos y el comportamiento de this son dos sistemas independientes en JavaScript. El valor de this se determina dinámicamente en el momento de la ejecución de una función y depende de cómo se invoca dicha función.

Los principales escenarios para this son:

  1. this en el Contexto Global: Fuera de cualquier función, en el ámbito global, this siempre se refiere al objeto global. En los navegadores web, este es el objeto window.
  2. this en Funciones:
    • Invocación Simple de Función: Cuando una función se llama directamente (ej., funcion()), this apunta al objeto global (window en navegadores) en modo no estricto. En modo estricto ('use strict'), this será undefined.
    • Invocación como Método de Objeto: Si una función es un método de un objeto y se invoca a través de ese objeto (ej., objeto.metodo()), this se refiere al objeto que posee el método (objeto en este caso).
  3. this en eval(): El valor de this dentro de una llamada a eval() será el mismo que el contexto de ejecución circundante, siempre que sea una llamada directa.

Consideremos un ejemplo para ilustrar el comportamiento de this:

const userProfile = {
 userName: "AliceDeveloper",
 displayUser: function() {
   console.log(`Usuario: ${this.userName}`);
 }
};

function getDisplayMethod() {
 const tempUser = "GuestUser"; // Variable local, no influye en 'this'
 return userProfile.displayUser; // Devolvemos la referencia a la función
}

// Declaración en el ámbito global usando let.
// ¡Importante! 'let' no añade la variable 'userName' al objeto global (window)
let userName = "GlobalScopeUser";

const displayFunc = getDisplayMethod();
displayFunc(); // ¿Qué es 'this' aquí? Se invoca como una función regular.
              // En modo no estricto, 'this' es el objeto global (window).
              // Como 'userName' fue declarada con 'let', 'window.userName' es 'undefined'.
              // Si se hubiera usado 'var userName = "GlobalScopeUser";', entonces 'this.userName' sería "GlobalScopeUser".

userProfile.displayUser(); // Aquí, 'this' es 'userProfile', por lo que imprime "Usuario: AliceDeveloper"

Este ejemplo demuestra cómo el valor de this cambia drásticamente dependiendo de la forma en que se ejecuta la función, un concepto clave para comprender la interacción de objetos y funciones en JavaScript.

Etiquetas: JavaScript ecmascript Hoisting Scope closures

Publicado el 7-20 20:24