Inicio / Artículos

Donde se escribe una función determina qué ve: el ámbito léxico de JavaScript

Comprenda cómo JavaScript resuelve el nombre de una variable a través de los entornos léxicos, por qué el lugar de la llamada nunca es relevante para la búsqueda, y cómo esto se aplica en los manejadores de React.

Editor: REACTAPP.TOP11 min de lectura2586 palabras

Fuentes: How JavaScript Finds Variables: Scope, Lexical Environment & Scope Chain

Cada vez que JavaScript evalúa un nombre como user, el motor debe decidir a qué enlace se refiere. El error más común, asumir que la función llamada puede ver las variables de su llamante, aparece tanto en entrevistas como en errores en producción. Esta guía aborda desde el ámbito hasta los entornos léxicos y la resolución de identificadores, dando especial énfasis a la diferencia entre la cadena de ámbitos y la pila de llamadas, para que pueda predecir cualquier problema con funciones anidadas y razonar claramente sobre los manejadores de React.

La pregunta: ¿cómo encuentra una función anidada las variables externas?

Comencemos con tres niveles anidados:

const appName = "HealthApp";

function dashboard() {
  const user = "Jashan";
  function showProfile() {
    const status = "Active";
    console.log(appName);
    console.log(user);
    console.log(status);
  }
  showProfile();
}

dashboard();

showProfile() declara solo status, sin embargo, todas las llamadas a console.log tienen éxito. Cada nombre se encuentra en un lugar diferente:

status  → showProfile()
user    → dashboard()
appName → global

Explicar esto requiere una breve secuencia de conceptos:

Scope
   ↓
Lexical Scope
   ↓
Lexical Environment
   ↓
Scope Chain
   ↓
Identifier Resolution

Ámbito: dónde se puede utilizar un nombre

El ámbito es la región del código en la que se puede hacer referencia a un identificador: responde a “¿desde dónde puedo usar este nombre?”

function dashboard() {
  const user = "Jashan";
  console.log(user); // ✅
}

dashboard();
console.log(user); // ❌ ReferenceError

user pertenece a dashboard(); el código fuera de ella genera un ReferenceError. Como árbol:

GLOBAL
│
└── dashboard()
      └── user

Lo cual da como resultado la regla:

Inside dashboard  → accessible ✅
Outside dashboard → inaccessible ❌

Por qué vale la pena tener aislamiento

Estas dos funciones declaran cada una una variable data:

function loadPatients() {
  const data = ["Patient A"];
}
function loadAppointments() {
  const data = ["Appointment A"];
}

Nunca chocan, porque cada una está dentro de su propio ámbito de función. Ese aislamiento permite que el código sea reutilizable y más fácil de depurar.

Los cuatro tipos de ámbito

JavaScript tiene cuatro tipos principales de ámbito:

Scope
├── Global Scope
├── Function Scope
├── Block Scope
└── Module Scope

Ámbito global

En un script clásico (no modular), las declaraciones de nivel superior pertenecen al ámbito global y son accesibles desde cualquier lugar:

var appName = "HealthApp";

function showAppName() {
  console.log(appName);
}

showAppName() encuentra appName porque su ruta de búsqueda termina en el entorno global. Un detalle: las declaraciones de nivel superior con var también se convierten en una propiedad del objeto global (window en los navegadores), mientras que las declaraciones de nivel superior con let y const no lo hacen. Mantenga el estado global al mínimo; esto vincula código distante y complica las pruebas.

Ámbito de la función

Cualquier cosa declarada dentro de una función es local a ella:

function calculateTotal() {
  const price = 100;
  const tax = 20;
  return price + tax;
}
console.log(price); // ❌

price y tax son variables locales, por lo que el registro final genera un error.

var ignora los bloques

var tiene su ámbito definido por la función más cercana, no por el par de llaves más cercano:

function example() {
  if (true) {
    var message = "Hello";
  }
  console.log(message); // ✅ Hello
}

El bloque if no contiene message, por lo que sigue siendo legible después del bloque.

Ámbito de bloque con let y const

let y const están confinados al bloque en el que se declaran:

if (true) {
  const message = "Payment successful";
  let status = "completed";
}

console.log(message); // ❌
console.log(status);  // ❌

Ambos registros fallan. Los bucles se comportan de la misma manera, y cada iteración de un bucle for (let ...) recibe una nueva asignación de i:

for (let i = 0; i < 3; i++) {
  console.log(i);
}
console.log(i); // ❌

Tenga en cuenta que los corchetes de un literal de objeto describen un valor, no un bloque, por lo que no crean ningún ámbito.

Ámbito de módulo

En los módulos ES, las declaraciones de nivel superior son privadas del archivo:

// auth.js
const token = "secret";
export function getUser() {}

Un módulo diferente no puede ver token:

// dashboard.js
console.log(token); // ❌

Solo lo que un módulo exporta explícitamente puede ser importado en otro lugar:

import { getUser } from "./auth.js";

Este es el ámbito cotidiano en React, Next.js, Vite y proyectos modernos de Node.js.

Ámbito léxico: la posición en el código fuente lo determina

JavaScript tiene ámbito léxico: una función puede acceder a las variables de todos los ámbitos que la contienen en el código fuente. Esa estructura se fija al analizar el código, antes de que se ejecute cualquier cosa.

const name = "Global";

function outer() {
  const name = "Outer";
  function inner() {
    console.log(name);
  }
  inner();
}

outer();

El programa imprime:

Outer

inner() está anidado dentro de outer() en el código fuente, por lo que name de outer es el más cercano:

GLOBAL
│
└── outer()
      ├── name = "Outer"
      │
      └── inner()

La búsqueda de name avanza desde inner hacia afuera y se detiene en outer:

inner
  ↓
not found locally
  ↓
outer
  ↓
name = "Outer" ✅

El ámbito léxico está determinado por dónde se define una función, nunca por dónde se llama.

La trampa en las entrevistas: definir versus llamar

Ahora mueva inner() fuera de outer(), mientras outer() todavía lo llame:

const name = "Global";
function outer() {
  const name = "Outer";
  inner();
}
function inner() {
  console.log(name);
}
outer();

Una suposición frecuente es:

Outer

El resultado real es:

Global

inner() está definido en el nivel superior, junto a outer(), no dentro de él.

GLOBAL
├── name = "Global"
├── outer()
└── inner()

Por lo tanto, su ruta de búsqueda va directamente al entorno global:

inner
  ↓
Global

No pasa por outer:

inner
  ↓
outer ❌

El hecho de que outer() haya invocado a inner() no cambia nada. El lugar donde se realiza la llamada da forma a la pila de llamadas; el lugar donde está definida determina el ámbito léxico.

Entornos léxicos: la mecánica detrás del ámbito

La especificación ECMAScript modela el ámbito mediante entornos léxicos. Cada uno registra dos cosas:

  1. los identificadores vinculados en ese entorno
  • un enlace al entorno que lo contiene
  • Como modelo mental:

    Lexical Environment
    │
    ├── Environment Record
    └── Outer Environment Reference
    

    Las ediciones recientes de la especificación incluyen el enlace exterior dentro del registro de entorno como un campo [[OuterEnv]]; el modelo no cambia.

    El registro de entorno almacena las asociaciones

    Tomemos una función con dos constantes locales:

    function dashboard() {
      const user = "Jashan";
      const role = "Admin";
    }
    

    Conceptualmente, su registro de entorno relaciona nombres con valores:

    dashboard Environment
    user → "Jashan"
    role → "Admin"
    

    Este es un modelo, no un objeto que su código pueda inspeccionar.

    La referencia exterior une los entornos

    Cada entorno apunta al que lo rodea léxicamente:

    const appName = "HealthApp";
    
    function dashboard() {
      const user = "Jashan";
    }
    

    Para dashboard(), se ve así:

    dashboard Environment
    │
    ├── user = "Jashan"
    └── Outer
          ↓
    Global Environment
    

    Ese puntero permite realizar búsquedas más allá del ámbito local. Se establece cuando se crea la función, en función de dónde fue definida, y por eso no importa el lugar desde donde se llama a la función.

    Rastreando toda la cadena

    Volviendo al ejemplo inicial, con los tres registros combinados:

    const appName = "HealthApp";
    
    function dashboard() {
      const user = "Jashan";
      function showProfile() {
        const status = "Active";
        console.log(appName, user, status);
      }
      showProfile();
    }
    
    dashboard();
    

    Cuando se ejecuta showProfile(), sus entornos están vinculados de la siguiente manera:

    SHOWPROFILE ENVIRONMENT
    │
    ├── status = "Active"
    └── Outer
          ↓
    DASHBOARD ENVIRONMENT
    │
    ├── user = "Jashan"
    └── Outer
          ↓
    GLOBAL ENVIRONMENT
    │
    ├── appName = "HealthApp"
    └── Outer
          ↓
         null
    

    La referencia externa del entorno global es null, lo que pone fin a la cadena.

    La cadena de ámbitos y la resolución de identificadores

    La cadena de ámbitos es el camino que sigue el motor para encontrar un identificador: se verifica el entorno actual y luego se sigue la referencia externa, hasta que se encuentra el nombre o termina la cadena.

    Current Environment
            ↓
    Outer Environment
            ↓
    Outer Environment
            ↓
    Global
            ↓
    null
    

    Encontrar el enlace a lo largo de ese camino se denomina resolución de identificadores. Tomemos las tres búsquedas:

    console.log(status);
    console.log(user);
    console.log(appName);
    

    Resolución de status

    Se encuentra inmediatamente en el entorno local:

    showProfile
         ↓
    status found ✅
    

    Resolución de user

    Falta localmente, se encuentra un nivel arriba:

    showProfile ❌
         ↓
    dashboard
         ↓
    user found ✅
    

    Resolución de appName

    Falta en ambas funciones, se encuentra en el entorno global:

    showProfile ❌
         ↓
    dashboard ❌
         ↓
    Global
         ↓
    appName found ✅
    

    En todos los casos, la búsqueda se detiene en el primer resultado. Para conocer más sobre el algoritmo de búsqueda en sí, consulte cómo resuelve JavaScript las variables mediante su cadena de ámbitos.

    Cuando falla la resolución

    Considere una función que intenta leer un nombre que nadie ha declarado:

    function showProfile() {
      console.log(age);
    }
    

    y una llamada a ella:

    showProfile();
    

    Si age no existe en ninguno de los entornos accesibles, la búsqueda finaliza:

    showProfile
         ↓
    Global
         ↓
    null
    

    El motor no puede resolver el nombre y lanza:

    ReferenceError: age is not defined
    

    Esto difiere de una variable que contiene undefined, o de un enlace let que aún no se ha inicializado (la zona muerta temporal). Estos tres estados son fáciles de confundir:

    undefined
    uninitialized
    undeclared
    

    Tres reglas que cubren la mayoría de los casos

    La búsqueda solo se realiza hacia afuera

    Inner
      ↓
    Outer
      ↓
    Global
    

    El código interno puede leer variables externas. El código externo no puede acceder a un ámbito anidado para leer variables declaradas únicamente allí.

    Gana el enlace más cercano

    Cuando existe el mismo nombre en varios niveles, se utiliza el más cercano:

    const user = "Global User";
    
    function dashboard() {
      const user = "Dashboard User";
      function showProfile() {
        console.log(user);
      }
      showProfile();
    }
    

    Salida:

    Dashboard User
    

    La búsqueda finaliza en dashboard, un nivel por encima:

    showProfile ❌
         ↓
    dashboard ✅
         ↓
    STOP
    

    El user global nunca se consulta. Una declaración interna que oculta a una externa de esta manera se denomina sombreado.

    Nada se copia a las funciones anidadas

    Una idea tentadora pero incorrecta es que las funciones internas reciben copias de las variables externas:

    showProfile
    ├── copied user
    └── copied appName
    

    La imagen precisa es una cadena de referencias:

    showProfile
         ↓
    dashboard
         ↓
    Global
    

    Al acceder a través de un enlace, la función interna siempre ve el valor actual del binding externo, y eso es lo que hace posible el funcionamiento de los cierres.

    Cadena de ámbitos versus pila de llamadas

    Aquí está nuevamente el ejemplo de la trampa:

    const name = "Global";
    
    function outer() {
      const name = "Outer";
      inner();
    }
    function inner() {
      console.log(name);
    }
    
    outer();
    

    Mientras se ejecuta inner(), la pila de llamadas se ve así:

    ┌──────────────────┐
    │    inner FEC     │ ← Current
    ├──────────────────┤
    │    outer FEC     │
    ├──────────────────┤
    │    Global EC     │
    └──────────────────┘
    

    outer() está claramente en la pila, ya que llamó a inner(). Sin embargo, inner() sigue imprimiendo:

    Global
    

    porque la búsqueda sigue la cadena de ámbitos, y la cadena de inner va directamente al entorno global. Estas dos estructuras responden a preguntas diferentes:

    • La pila de llamadas registra los contextos de ejecución que están actualmente activos; la cadena de ámbito resuelve los identificadores.
    • La pila de llamadas está ordenada según qué función llama a cuál; la cadena de ámbito se forma según cómo está anidado el código fuente.
    • La pila de llamadas sigue el principio último en entrar, primero en salir; la cadena de ámbito sigue los enlaces al entorno exterior.
    • La pila de llamadas responde a “¿qué está ejecutándose en este momento?”; la cadena de ámbito responde a “¿dónde existe este nombre?”

    Contextos de ejecución y entornos léxicos

    Contexto de ejecución

    Un contexto de ejecución almacena el estado necesario mientras se ejecuta el código. Los dos tipos principales son:

    Global Execution Context
    Function Execution Context
    

    Entorno léxico

    Un entorno léxico almacena las asociaciones de identificadores y el enlace a su entorno exterior. Un contexto de ejecución hace referencia a su entorno, más o menos de esta manera:

    Execution Context
          │
          └── environment information
                   ↓
           Lexical Environment
                   │
                   ├── bindings
                   └── outer environment
    

    Por lo tanto, las dos cosas no son lo mismo:

    Execution Context ≠ Lexical Environment
    

    El contexto de ejecución explica cómo se ejecuta el código; el entorno léxico explica cómo se encuentran los nombres.

    Ámbito léxico en componentes de React

    El código de React depende constantemente del ámbito léxico. Considere un manejador de eventos definido dentro de un componente:

    function UserDashboard({ userId }: { userId: string }) {
      const role = "Admin";
    
      function handleProfileClick() {
        console.log(userId);
        console.log(role);
      }
    
      return (
        <button onClick={handleProfileClick}>
          View Profile
        </button>
      );
    }
    

    handleProfileClick() no declara ninguno de estos nombres:

    userId
    role
    

    Aun así los utiliza porque está escrito dentro de UserDashboard:

    handleProfileClick
            ↓
    UserDashboard
            ├── userId
            └── role
    

    El mismo mecanismo se aplica a props, estado, callbacks, temporizadores, promesas y hooks. Cada renderización crea un nuevo entorno con los valores de esa renderización, y los manejadores definidos durante ella tienen acceso exclusivo a esos valores. Esa es la base de las cierres, y de los cierres obsoletos, en los que un callback de una renderización anterior sigue leyendo estado antiguo.

    Conceptos erróneos comunes

    “La búsqueda sigue al que llama”

    No es así. Lo que importa es el sitio de definición:

    Where function is called ❌
    Where function is defined ✅
    

    “La pila de llamadas y la cadena de ámbitos son lo mismo”

    Tienen propósitos diferentes:

    Call Stack  → execution
    Scope Chain → identifier lookup
    

    “El código exterior puede leer variables internas”

    Inner → Outer → Global ✅
    Outer → Inner ❌
    

    “var respeta los bloques”

    No lo hace; solo let y const sí lo hacen:

    var       → function-scoped
    let/const → block-scoped
    

    “Un entorno léxico es un objeto ordinario”

    Es un concepto de especificación; los motores lo implementan libremente, y tu código no puede acceder a él.

    Un modelo mental para recordar

    El tema se resume en una cadena de preguntas:

    Scope
       ↓
    Where can I use this variable?
       ↓
    Lexical Scope
       ↓
    Where was this function defined?
    Lexical Environment
       ↓
    What variables exist here?
    Outer Environment
       ↓
    Where do I look next?
    Scope Chain
       ↓
    Current → Outer → Global
    Identifier Resolution
       ↓
    Find the matching variable
    

    Puntos clave

    • El ámbito determina dónde se puede utilizar un nombre; JavaScript cuenta con ámbitos global, de función, de bloque y de módulo, siendo var limitado a las funciones y let/const a los bloques.
    • El ámbito es léxico: el entorno exterior de una función está fijado en el lugar donde se define, por lo que llamarla desde otro lugar nunca cambia lo que puede acceder.
    • La resolución de identificadores avanza desde el entorno actual hacia afuera, se detiene en la primera coincidencia (sombreado) y lanza un ReferenceError si llega al final.
    • Las funciones anidadas conservan referencias a los entornos externos, no copias, lo cual constituye la base de los cierres y de los cierres obsoletos en React.

    Lecturas relacionadas