Головна / Статті

Те, де написана функція, визначає, що вона бачить: лексичний діапазон JavaScript

Зрозумійте, як JavaScript визначає ім’я змінної через лексичні середовища, чому місце виклику ніколи не має значення під час пошуку, та як це впливає на обробники подій у React.

Видавець: REACTAPP.TOP11 хв читання2586 слів

Джерела: How JavaScript Finds Variables: Scope, Lexical Environment & Scope Chain

Кожного разу, коли JavaScript обробляє ім’я на кшталт user, движок мусить визначити, до якого зв’язку воно посилається. Найпоширеніша помилка — припущення, що функція-об’єкт може бачити змінні свого викликаючого коду — трапляється як під час співбесід, так і при роботі програми. У цьому посібнику ми розглядаємо від контексту до лексичних середовищ та процесу резолюції ідентифікаторів, приділяючи особливу увагу різниці між ланцюгом контекстів та стеком викликів, щоб ви могли передбачати будь-які складні ситуації з вкладеними функціями та чітко мислити щодо обробників подій у React.

Питання: як вкладена функція знаходить зовнішні змінні?

Почнемо з трьох рівнів вкладеності:

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() оголошує лише status, проте всі три виклики console.log успішні. Кожне ім’я знаходиться в різному місці:

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

Пояснення цього потребує короткої послідовності концепцій:

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

Область видимості: де можна використовувати ім’я

Область видимості — це частина коду, в якій можна посилатися на ідентифікатор: вона відповідає на запитання «Звідки я можу використовувати це ім’я?»

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

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

user належить до dashboard(); код за межами неї викликає помилку ReferenceError. У вигляді дерева:

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

Це дає наступне правило:

Inside dashboard  → accessible ✅
Outside dashboard → inaccessible ❌

Чому важлива ізоляція

Кожна з цих двох функцій оголошує змінну data:

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

Вони ніколи не конфліктують, оскільки кожна знаходиться у власній області видимості функції. Ця ізоляція робить код багатократно використовуваним та простішим у дебагуванні.

Чотири типи областей видимості

У JavaScript є чотири основні типи областей видимості:

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

Глобальна область видимості

У класичному (немодульному) скрипті оголошення верхнього рівня належать до глобального простору імен та є доступними з будь-якого місця:

var appName = "HealthApp";

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

showAppName() знаходить appName, оскільки шлях пошуку закінчується у глобальному середовищі. Одна деталь: змінні верхнього рівня типу var також стають властивостями глобального об’єкта (window у браузерах), тоді як змінні верхнього рівня типу let та const — ні. Зберігайте глобальний стан у мінімумі; це поєднує віддалений код та ускладнює тестування.

Простір імен функції

Усе, що оголошено всередині функції, є локальним для неї:

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

price та tax є локальними, тому остаточний запис у журнал викликає помилку.

var ігнорує блоки

var має простір імен, обмежений найближчою функцією, а не найближчою парою дужок:

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

Блок if не містить змінної message, тому він залишається читабельним після завершення блоку.

Область видимості блоку з let та const

let та const обмежені блоком, у якому вони оголошені:

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

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

Обидва записи у журналі помилок не спрацювали. Цикли поводяться однаково, і кожна ітерація циклу for (let ...) отримує нове значення змінної i:

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

Зауважте, що дужки об’єктного літералу описують значення, а не блок, тому вони не створюють області видимості.

Область видимості модуля

У ES-модулях оголошення верхнього рівня є приватними для файлу:

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

Інший модуль не може бачити змінну token:

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

Лише те, що модуль явно експортує, може бути імпортовано в іншому місці:

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

Це є звичайним областю видимості в проектах React, Next.js, Vite та сучасних Node.js.

Лексична область видимості: визначається положенням у коді

JavaScript має лексичну область видимості: функція бачить змінні всіх областей видимості, які її оточують у коді. Ця структура фіксується під час парсингу коду, ще до того, як щось буде виконано.

const name = "Global";

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

outer();

Програма виводить:

Outer

inner() розташована всередині outer() у коді, тому змінна name з області видимості outer є найближчою:

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

Пошук змінної name відбувається зсередини inner назовні та зупиняється на outer:

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

Лексична область видимості визначається місцем, де визначена функція, а не місцем її виклику.

Пастка на співбесіді: визначення проти виклику

Тепер виведіть inner() за межі outer(), при цьому outer() все ще має його викликати:

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

Поширеною припущенням є:

Outer

Фактичний результат є таким:

Global

inner() визначений на верхньому рівні, поруч із outer(), а не всередині нього.

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

Тож шлях пошуку йде безпосередньо до глобального середовища:

inner
  ↓
Global

Він не проходить через outer:

inner
  ↓
outer ❌

Те, що outer() викликає inner(), нічого не змінює. Місце виклику формує стек викликів; місце визначення визначає лексичний діапазон.

Лексичні середовища: механізм діапазонів

Специфікація ECMAScript моделює діапазони за допомогою лексичних середовищ. Кожне з них записує дві речі:

  1. ідентифікатори, прив’язані до цього середовища
  • посилання на середовище, яке його оточує
  • Як ментальна модель:

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

    У новіших версіях специфікації зовнішнє посилання включається до запису середовища як поле [[OuterEnv]]; модель залишається незмінною.

    Запис середовища містить зв’язки

    Візьмемо функцію з двома локальними константами:

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

    Концептуально її запис середовища відповідає зв’язку імен з значеннями:

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

    Це модель, а не об’єкт, який можна перевірити у вашому коді.

    Зовнішнє посилання об’єднує середовища

    Кожне середовище вказує на те, яке його лексично оточує:

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

    Для dashboard() це виглядає так:

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

    Цей вказівник дозволяє здійснювати пошук поза межами локального області видимості. Він встановлюється під час створення функції, залежно від місця її визначення, тому місце виклику не має значення.

    Відстеження повної ланцюгової структури

    Повернімося до початкового прикладу з об’єднаними трьома записами:

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

    Коли виконується showProfile(), його середовища пов’язані таким чином:

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

    Зовнішній посилання глобального середовища дорівнюють null, що завершує ланцюг.

    Ланцюг областей видимості та резолюція ідентифікаторів

    Ланцюг областей видимості — це шлях, яким двигун знаходить ідентифікатор: спочатку перевіряється поточне середовище, потім слідується за зовнішніми посиланнями, поки ім’я не знаходиться або ланцюг не завершується.

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

    Знаходження відповідного зв’язку по цьому шляху називається резолюцією ідентифікатора. Розгляньмо три випадки пошуку:

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

    Розв’язування проблеми з status

    Знайдено безпосередньо в локальному середовищі:

    showProfile
         ↓
    status found ✅
    

    Розв’язування проблеми з user

    Відсутнє в локальному середовищі, знайдено на рівень вище:

    showProfile ❌
         ↓
    dashboard
         ↓
    user found ✅
    

    Розв’язування проблеми з appName

    Відсутнє в обох функціях, знайдено у глобальному середовищі:

    showProfile ❌
         ↓
    dashboard ❌
         ↓
    Global
         ↓
    appName found ✅
    

    У кожному випадку пошук зупиняється при першій знахідці. Щоб детальніше дізнатися про алгоритм пошуку, перегляньте як насправді JavaScript розв’язує змінні за допомогою ланцюга областей.

    Коли розв’язування зазнає невдачі

    Розгляньмо функцію, яка намагається отримати доступ до імені, яке ніхто не оголосив:

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

    та виклик цієї функції:

    showProfile();
    

    Якщо age відсутнє в жодному з доступних середовищ, пошук завершується:

    showProfile
         ↓
    Global
         ↓
    null
    

    Движок не може знайти ім’я та кидає помилку:

    ReferenceError: age is not defined
    

    Це відрізняється від змінної, яка містить значення undefined, або від зв’язку типу let, який ще не ініціалізований (тимчасова зона смерті). Ці три стани легко плутати:

    undefined
    uninitialized
    undeclared
    

    Три правила, які охоплюють більшість випадків

    Пошук відбувається лише назовні

    Inner
      ↓
    Outer
      ↓
    Global
    

    Внутрішній код може читати змінні зовнішнього рівня. Зовнішній код не може отримати доступ до змінних, оголошених лише у вкладеному області видимості.

    Перемагає найближчий зв’язок

    Коли однакове ім’я існує на кількох рівнях, використовується найближчий зв’язок:

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

    Результат:

    Dashboard User
    

    Пошук завершується у розділі dashboard, на один рівень вище:

    showProfile ❌
         ↓
    dashboard ✅
         ↓
    STOP
    

    Глобальна змінна user ніколи не використовується. Оголошення всередині, яке приховує оголошення ззовні, називається затінюванням.

    Нічого не копіюється до вкладених функцій

    Привабливим, але хибним уявленням є те, що внутрішні функції отримують копії зовнішніх змінних:

    showProfile
    ├── copied user
    └── copied appName
    

    Правильне уявлення — це ланцюг посилань:

    showProfile
         ↓
    dashboard
         ↓
    Global
    

    Читання через посилання означає, що внутрішня функція завжди бачить поточне значення зовнішньої змінної, і саме це дозволяє працювати з закритими функціями.

    Ланцюг областей видимості проти стеку викликів

    Ось знову приклад пастки:

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

    Під час виконання inner() стек викликів виглядає так:

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

    outer() однозначно знаходиться у стеку, оскільки вона викликала inner(). Проте inner() все одно виводить:

    Global
    

    тому що пошук відбувається за ланцюгом областей видимості, а ланцюг inner веде безпосередньо до глобального середовища. Ці дві структури відповідають на різні запитання:

    • Стек викликів відстежує контексти виконання, які наразі є активними; ланцюг областей видимості вирішує ідентифікатори.
    • Стек викликів упорядкований за тим, яка функція викликала іншу; ланцюг областей видимості формується залежно від того, як у коді розташовані вкладені структури.
    • У стеку викликів діє принцип «останній доданий — перший видалений»; ланцюг областей видимості слідує за посиланнями на зовнішнє середовище.
    • Стек викликів відповідає на запитання «що зараз виконується?»; ланцюг областей видимості відповідає на запитання «де знаходиться цей ідентифікатор?»

    Контексти виконання та лексичні середовища

    Контекст виконання

    Контекст виконання зберігає стан, необхідний під час виконання коду. Існують два основні типи:

    Global Execution Context
    Function Execution Context
    

    Лексичне середовище

    Лексичне середовище зберігає посилання на ідентифікатори та зв’язок із його зовнішнім середовищем. Контекст виконання посилається на своє середовище приблизно так:

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

    Отже, це дві різні речі:

    Execution Context ≠ Lexical Environment
    

    Контекст виконання пояснює, як працює код; лексичне середовище пояснює, як знаходяться імена.

    Лексичний діапазон у компонентах React

    Код React постійно спирається на лексичний діапазон. Розгляньмо обробник подій, визначений всередині компонента:

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

    handleProfileClick() не оголошує жодного з цих імен:

    userId
    role
    

    Проте він їх все одно використовує, оскільки написаний всередині UserDashboard:

    handleProfileClick
            ↓
    UserDashboard
            ├── userId
            └── role
    

    Той самий механізм застосовується до props, state, обробників подій, таймерів, обіцянок та хуків. Кожне відображення створює нове середовище з значеннями цього відображення, а обробники, визначені під час нього, опираються саме на ці значення. Це є основою замикань, а також застарілих замикань, коли обробник подій з попереднього відображення продовжує читати старий state.

    Поширені хибні уявлення

    "Пошук відбувається за місцем виклику"

    Це не так. Важливим є місце визначення:

    Where function is called ❌
    Where function is defined ✅
    

    ">Стек викликів та ланцюг областей видимості — це одне й те саме"

    Вони виконують різні функції:

    Call Stack  → execution
    Scope Chain → identifier lookup
    

    ">Зовнішній код може читати змінні внутрішнього блоку"

    Пошук є однонапрямковим:

    Inner → Outer → Global ✅
    Outer → Inner ❌
    

    "var дотримується обмежень блоків"

    Це не так; лише let та const дотримуються цих обмежень:

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

    ">Лексичне середовище — це звичайний об’єкт"

    Це концепція специфікації; движки реалізують її на власний розсуд, і ваш код не може до неї отримати доступу.

    Ментальна модель, яку потрібно зберігати

    Ця тема може бути узагальнена у ланцюжок запитань:

    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
    

    Основні висновки

    • Область видимості визначає, де можна використовувати ім’я; у JavaScript існують глобальна, функційна, блокова та модульна області видимості, причому var належить до функційної області, а let/const — до блокової.
    • Область видимості є лексичною: зовнішнє середовище функції зафіксоване там, де вона визначена, тому її виклик з іншого місця ніколи не змінює того, що вона може бачити.
    • Резолюція ідентифікатора відбувається з поточного середовища назовні, зупиняється при першому збігу (затіненні) та генерує помилку ReferenceError, якщо доходить до кінця.
    • Внутрішні функції зберігають посилання на зовнішнє середовище, а не його копії, що є основою замикань та застарілих замикань у React.