Там, дзе запісана функцыя, вялічыць тое, што яна бачыць: лексычны дыялект JavaScript
З'ясавайце, як JavaScript вырашвае назву зменнай через лексычныя среды, чаму месца вызову ніколі не мае значэння для пошуку, і як гэта паўтарыцца ў обробніках React.
Крыніцы: 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 моделюе сферу за дапамою лексычных сераўысаў. Кожны з іх фіксуе два элементы:
- ідэнтыфікаторы, прыязначаныя ў тым сераўысе
Як ментальная модэль:
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, прыемнікаў, таймераў, promises і hooks. Кожны рендер стварае новую среду з значэннямі гэтага рендера, а прыемнікі, заданыя ў яго час, вжываюць саме гэтыя значэння. Гэта є база клозураў, а таксама «старых» клозураў, калі прыемнік з паканальнага рендера продовжвае чытаць стары 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— да блаковых. - Рэгламент вялікасці ў JavaScript являе сабою лексычны прынцып: зовнішняе сераўака функцыі зафіксавана там, дзе яна была адзначана, таму ўвокаліванне яе з іншага месца ніколі не змянюе таго, што яна можа бачыць.
- Процес аднавлення ідэнтыфікатора выкананы з ныявечнага сераўака на зовнішнія, зупыняецца на першам падабенстве (затэніванні), а якщо дасягне канца, выклікаецца
ReferenceError. - Внутранія функцыі зберагаюць канкрэтныя аднасуненні да зовнішняго сераўака, а не копіі, і гэта являецца адной з базаў клозураў, а таксама прычыной старых клозураў у React.