Раз і назавжди розбираємось з Lexical environment, Scope та Closures в Javascript
Добрий день!
Я знаю, що в епоху тіктоку ніхто не читає вступи. Але мене в школі вчили, що розмова починається з Добрий день, а твір зі вступу. Тому:
Вступ
Я Андрій (лінкедин). Вже чимало часу професійно програмую на js, і коли є настрій, то якщо вірити людям, досить непогано отекстовую свої думки і пояснюю складні речі простіше.
Минулої зими після звільнення з армії щоб трохи відволіктись від гнітючої реальності освіжити і заодно систематизувати знання я прям повністю перечитав javascript.info. Причому не просто пролистав, а редагував переклад на українську. Прям вчитувався в кожне слово як вегетаріанець в склад ковбаси. І хоч це не зробило з мене Дугласа Крокфорда, але щось в javascript та й почав розуміти.
Ну і власне сьогодні розкажу (надіюсь людською мовою) що таке lexical environment, що таке scope, closure, в чому різниця. Як вони працюють, трохи про як взагалі функції в js працюють. В цій темі дуже багато путанини і навіть досвідчені програмісти часто путаються. Тому щоб менше путатись поїхали:
Спершу коротко
- Lexical Environment — воно ж одночасно лексичне оточення і лексичне середовище. Обидва варіанти перекладу правильні. Питання лише вподобання. І власне цей lexical environment — це цілком реальний об’єкт в пам’яті пристрою. Це прихований об’єкт, ми ніяк з нашого коду до нього дістати не можемо, але тим не менш це цілком реальний об’єкт. Завдяки якому рушій js працює саме так, як працює.
- Closure — замикання — це теоретичний термін, який має на увазі саму функцію плюс прикріплені до неї lexical environment’и.
- Scope — це власне область видимості, «список видимого», «список доступного». Це теж теоретичний термін. Але він має на увазі «сукупність усіх видимих змінних з тої чи іншої точки коду».
Якщо все ясно, то далі можна не читати.
А якщо ні, то поїхали розбиратись в деталях:
Функція в javascript — що це взагалі таке і як воно працює
Функція в javascript — це об’єкт. Callable об’єкт, і тим не менш об’єкт.
Так, саме так, функція в javascript — це об’єкт!
Якщо не віриш, то запусти в консолі цей код і переконайся
function doSmth() {}
console.log(doSmth instanceof Object); // true
Для ще більшої переконливості:
function sayHi() {
if (sayHi.userName) {
console.log(`Привіт, ${sayHi.userName}!`)
} else {
sayHi.userName = prompt("Привіт! Введи своє ім'я:");
sayHi()
}
}
sayHi.userName = "Андрій";
sayHi() // "Привіт, Андрій!"
const borkedObject = sayHi;
borkedObject.userName = "Степан";
sayHi() // "Привіт, Степан!"
borkedObject() // "Привіт, Степан!"
console.log(typeof borkedObject) // "function"
delete borkedObject.userName;
sayHi() // спершу prompt, а потім "Привіт, {твоє значення}!"
console.log(borkedObject.userName) // введене тобою значення
const pristineObject = {
...sayHi
};
console.log(pristineObject) // { userName: введене тобою значення }
У мене коли я вперше про це дізнався була думка: це ж дурість! Це ж суперечить логіці! Нащо робити функцію об’єктом і взагалі що між ними спільного?! Об’єкт придуманий щоб берегти якісь дані, а функція придумана щоб щось робити! Змішали бульдога й носорога. Який жаах, який несмак, ааа.
Але потім я видихнув, подумав трошки довше і зрозумів:
Книжка з рецептами не пече піцу. Книжка з рецептами всього лиш береже набір інструкцій.
Функція в javascript — це теж така собі книжка з рецептами. Коли викликається sayHi() — це рушій javascript починає виконувати записані в функцію інструкції. Це рушій javascript починає глядіти і міняти значення змінних, викликати браузерні API (console.log, prompt) і т д. А функція сама по собі лише береже набір тих інструкцій і все.
Уяви якби замість:
const logSomething = function (something) {
console.log(something);
};
ми писали б:
const logSomething = new Function( // пишемо які параметри приймати "data", // пишемо що робити "console.log(data)", );
І уяви якби в результаті в нас до змінної з назвою logSomething записувалась не якась там магічна абракадабра, а об’єкт:
{
"parameters": ["data"],
"whatToDo": "console.log(data)"
}
Уявив/ла?
Так от саме так це і працює. Запусти в консолі цей код і переконайся:
function doSmth() {}
console.log(doSmth instanceof Function);
console.log(Object.prototype.isPrototypeOf(Function));
В старі добрі часи коли трава була зеленіша, а українці безтурботно продавали танки в Африку і про війну ніхто навіть не думав. В ті самі добрі часи, коли вода була рідкіша, а Міндіч замість посилати в Москву двушечки возив туди Зеленського на гастролі. В ті самі добрі часи, коли я тихо ходив під стіл, Янукович тихо крав шапки, а браузери тихо ігнорували питання безпеки, в ті самі старі добрі часи можна було запустити const logSomething = new Function("data", "console.log(data)") і це спрацювало б. Але на вулиці 2026 рік, світ зараз трохи інший і зараз скоріше всього браузер видасть помилку. І тим не менш, помилка буде This document requires 'TrustedScript' assignment. The action has been blocked., тобто рушій цілком розуміє new Function(), просто з міркувань безпеки браузер обриває виконання.
Стоп. А до чого тут lexical environment?
Твоя правда, діда щось понесло. Вертаємось до lexical environment:
В прикладі вище все дуже просто. Всередині функції ми використовуємо лише змінну data яка приходить як аргумент.
Але що якщо ми захочемо звернутись до якоїсь зовнішньої змінної?
Звідки сáме рушій має брати значення тієї зовнішньої змінної? Га?
З місця де функцію було оголошено? Так. Але як рушію знати де функцію було оголошено?
Рушій сам по собі нічого не знає. Рушій — це просто програма. Щоб він щось «знав» — він має це щось якось зберегти в пам’яті пристрою.
Так от щоб цей рушій працював саме так, як він працює, програміст, який створював цей рушій, придумав lexical environment.
Коли рушій починає виконувати код, рушій для кожного окремого блоку коду створює в пам’яті пристрою окремий lexical environment. В тих lexical environment’ах рушій береже [[Environment Record]] — підоб’єкт, де містяться всі змінні, які були оголошені всередині того блоку коду, і [[Reference to the Outer Lexical Environment]] — посилання на lexical environment зовнішнього блоку.
Щоб було легше це уявити і запам’ятати розглянемо приклад:
// lex-env-0 для всього скрипта
// (в скрипта нема фігурних дужок, але він теж як один великий блок коду)
let animal = "jeżyk";
if (true) { // lex-env-1
animal = "bóbr";
}
if (true) { // lex-env-2
const nickname = "ku_wa";
if (true) { // lex-env-3
const salutation = "ja pierdolę, jakie bydlę!";
console.log(animal, nickname, salutation);
console.log(unexistingSmth);
}
}
Коли рушій починає виконувати код вище, він під капотом створює назвемо це сторедж лексичних оточень який спрощено можна зобразити ось так:
{
"lex-env-0": {
"Environment_Record": {
"animal": "bóbr"
},
"Outer_LE_id": null
},
"lex-env-1": {
"Environment_Record": {},
"Outer_LE_id": "lex-env-0"
},
"lex-env-2": {
"Environment_Record": {
"nickname": "ku_wa"
},
"Outer_LE_id": "lex-env-0"
},
"lex-env-3": {
"Environment_Record": {
"salutation": "ja pierdolę, jakie bydlę!"
},
"Outer_LE_id": "lex-env-2"
}
}
Коли на рядку 7 (animal = "bóbr") в коді сказано, що треба змінити значення змінної animal, то рушій спершу намагається знайти змінну з назвою animal в лексичному середовищі того самого блоку (lex-env-1). Але там цієї змінної нема. Тому він переходить далі за Reference to the Outer Lexical Environment (посиланням на зовнішнє лексичне середовище) в lex-env-0 і от там ця змінна є. І от там він її міняє.
Коли на рядку 16 треба виконати console.log(animal, nickname, salutation), то рушій спершу намагається знайти змінну із назвою animal в лексичному середовищі того самого блоку lex-env-3, але там такої змінної нема. Тоді він переходить за Reference to the Outer Lexical Environment в lex-env-2, але там змінної animal теж нема. І так він переходить за посиланням в lex-env-0 і знаходить її там. Потім рушій за тим же принципом глядить змінну з назвою nickname і змінну з назвою salutation.
А коли на рядку 20 треба зробити console.log(unexistingSmth), то рушій так само глядить unexistingSmth спершу в lex-env-3, потім в lex-env-2, потім в lex-env-0, і так як lex-env-0 не має Reference to the Outer Lexical Environment, то рушій обірве виконання нашого коду і покаже помилку «unexistingSmth is not defined».
Повертаємось до функцій
Вище ми вже з’ясували, що функція — це об’єкт, який береже в собі код який треба виконати та параметри які треба випросити.
Також ми з’ясували, що для кожного блоку коду (для кожного блоку коду, не лише для функцій) рушій створює в пам’яті пристрою lexical environment де береже всі змінні оголошені в тому блоці коду та посилання на lexical environment батьківського блоку коду.
Настав час скласти пазл 🪄
В прикладі який ми розбирали вище, всі блоки коду виконуються лише один раз і в одному місці. Але функцію рушій може запускати багато разів і в багатьох різних місцях.
Кожен раз, коли рушій буде намагатись виконати код який зберігається в тій функції, кожен раз рушій буде мусити знайти десь значення змінних, до яких звертається код тієї функції.
І власне для цього Брендан Ейх (творець javascript) вирішив, що функція буде зберігати не лише код який треба виконати і параметри які треба отримати, але також буде зберігати те саме Reference to the Outer Lexical Environment.
Тому коли ми пишемо
const logSomething = function (data) {
console.log(data);
};
то рушій в цей момент створює new Function() яка береже в собі не лише whatToDo: "console.log(data)" і parameteres: ["data"], а також береже посилання на лексичне середовище того блоку коду, де функція була оголошена. Причому посилання на лексичне середовище того блоку коду, в якому функцію було оголошено, зберігається з функцією назавжди. Навіть коли ми функцію передаємо кудись як параметр або копіємо. Функція завжди береже reference на лексичне середовище блока, де була оголошена.
І саме ця особливість дозволяє робити трюки по типу цього:
let incrementAndReturnClosedCounter;
if (true) {
let closedCounter = 0;
incrementAndReturnClosedCounter = function () {
return ++closedCounter;
};
}
console.log(incrementAndReturnClosedCounter()); // 1
console.log(incrementAndReturnClosedCounter()); // 2
console.log(incrementAndReturnClosedCounter()); // 3
closedCounter++; // Помилка. closedCounter is not defined
Звертаю увагу на дуже важливий нюанс!
Всередині функції зберігається не сам lexical environment, а лише посилання на lexical environment, причому посилання на lexical environment батьківського блока коду. «Вшивається» в функцію лише посилання на зовнішнє лексичне середовище.
Але!
Власне лексичне середовище (лексичне середовище самої функції в сенсі лексичного посилання для самого того коду, який бережеться в функції) створюється заново при кожному запуску того коду, який записаний в функцію.
Ще раз: посилання, посилання на lexical environment зовнішнього блоку коду зберігається в функції назавжди. Але сам безпосередньо lexical environment запущеного коду який зберігається в функції при кожному запуску створюється заново.
Тому всі змінні, які оголошуються всередині функції, при кожному запуску функції створюються заново.
const incrementAndReturnInnerCounter = function () {
let innerCounter = 0; // він щоразу створюється заново
innerCounter++;
return innerCounter;
};
console.log(incrementAndReturnInnerCounter()); // 1
console.log(incrementAndReturnInnerCounter()); // 1
console.log(incrementAndReturnInnerCounter()); // 1
Посилання (reference) на зовнішнє лексичне середовище передається внутрішньому лексичному середовищу. Але саме внутрішнє лексичне середовище створюється щоразу нове.
Вправи для закріплення
Глянь цей код і подумай чому він не кидає помилку. Чому sayHi який звертається до sayHi не дає помилку?
const sayHi = function () {
if (sayHi.userName) {
console.log(`Hi, ${sayHi.userName}!`);
} else {
sayHi.userName = prompt("Hi! What is your name?");
sayHi();
}
};
sayHi();
Коли ми пишемо function sayHi() {..... — це по суті те саме, що сказати const sayHi = new Function(.... . Тобто рушій в environment record всередині lexical environment’а цілого скрипта створює змінну з назвою sayHi і до тої змінної запихає об’єкт-функцію яка береже в собі код який треба виконати і outer reference на лексичне середовище блока де ця функція була створена:
{
"lex-env-0": { // lexical environment цілого скрипта
"Environment_Record": {
"sayHi": {
"type": "function",
"whatToDo": "if(sayHi.userName){console.log(`Hi...",
"parameters": [],
"reference_to_outer_LE": "lex-env-0"
}
},
"Outer_LE_id": null
}
}
Після цього, коли ми пишемо sayHi() — ми даємо наказ виконати код всередині тієї функції. Рушій створює лексичне середовище для цього конкретного запуску функції (lex-env-1) і пам’ять починає виглядати приблизно так:
{
"lex-env-0": { // lexical environment цілого скрипта
"Environment_Record": {
"sayHi": {
"type": "function",
"whatToDo": "if(sayHi.userName){console.log(`Hi...",
"parameters": [],
"reference_to_outer_LE": "lex-env-0"
}
},
"Outer_LE_id": null
},
"lex-env-1": { // lexical environment для цього конкретного запуску функції sayHi
"Environment_Record": {}, // порожньо, бо внутрішніх змінних в функції не оголошено
"Outer_LE_id": "lex-env-0" // воно передалось від reference_to_outer_LE самої функції
}
}
Тоді рушій починає крок за кроком читати і виконувати інструкції, які написані в тій функції. Коли діло доходить до if(sayHi.userName) — рушій спершу намагається знайти змінну з назвою sayHi в lex-env-1, але там її нема. Тоді рушій переходить за reference до lex-env-0 і от там рушій спокійно знаходить sayHi. І тому помилки нема, все працює.
Тепер спробуй самостійно спрогнозувати що буде в цьому випадку:
function createSayAbrakadabra() {
return function () {
console.log(sayAbracadabra);
};
}
const greet = createSayAbrakadabra();
greet();
і що буде в цьому:
function createSayAbrakadabra() {
return function () {
console.log(sayAbracadabra);
};
}
const greet = createSayAbrakadabra();
let sayAbracadabra = "Abrakadabra!";
greet();
sayAbracadabra = createSayAbrakadabra();
greet();
sayAbracadabra();
createSayAbrakadabra()();
Щоб не було спойлерів я не писав нічого. Перевірити правильність своїх думок можеш запустивши код. Якщо з’являться питання — чекатиму в коментарях.
Якщо твої відповіді неправильні — раджу перечитати цей матеріал ще раз через кілька днів.
Про scope та closure
Вище я багато говорив про lexical environment (бо це найважче). І припускаю, що непогано так забив голову.
І вибач, але так працює цей світ. Якби зрозуміти як працює js можна було одним коротким відео в тікток, то розуміння js не було б цінністю і ніхто б за це не платив. На жаль (чи на щастя) це не так. Тому терпи, муч свою голову :)
Так от.
Як я вже казав — lexical environment — це цілком реальний об’єкт. Лексичні середовища реально існують в пам’яті пристрою.
Натомість scope та closure — це теоретичні терміни. Це слова, якими намагаються описати явища.
Closure — замикання — це слово, під яким мається на увазі функція (тобто її власний lexical environment який було створено під час запуску функції) ПЛЮС усі лексичні середовища, які доступні цій функції (по ланцюжку через outer reference на батьківський блок коду).
Scope — область видимості — це слово, під яким мається на увазі змінні, які доступні з того чи іншого місця в коді.
Ну тобто приклад:
const userName = "Василь";
function getRole() {
const randomNumber = Math.random();
if (randomNumber > 0.9) {
return "прокурор";
} else if (randomNumber > 0.7) {
return "ІТвець";
} else {
return "кріпак";
}
}
Скільки і яких lexical environment’ів створить рушій — ти можеш порахувати самостійно.
Замикання тут — це сама функція getRole разом із посиланням на зовнішній lexical environment, де бережуться userName і getRole.
А область видимості (scope) залежить від того, з якої точки в коді ми дивимось. Якщо наприклад з рядка 4, то до scope в тому місці входить randomNumber, getRole та userName. Якщо ж дивимось на код з рядка 14, то з того місця в scope входить лише userName та getRole.
Поправка на вітер
Іноді scope трактують не як сукупність змінних, а як правило, за яким визначається які змінні має бути видно.
На жаль так історично склалось. От не змогли розумні люди придумати два різних слова. На жаль розплодили таку плутанину коли під одним словом (scope) може матись два схожих, але дуже різних поняття.
Конкретно я, конкретно тут, розглядаю scope в тому ж значенні, під яким воно мається на увазі в devtools і загалом в більшості випадків. Тобто як сукупність змінних, які доступні.
Ще раз коротко. Щоб підсумувати
Lexical environment’и — лексичні середовища — це цілком реальні об’єкти в пам’яті пристрою. Це частина механізму, частина рушія, завдяки якій рушій пам’ятає де яка змінна.
Closure — замикання — це теоретичний термін, який розшифровується як «функція та змінні, які їй доступні».
Scope — область видимості — це власне область видимості (як на мене переклад цілком передає суть) — це сукупність змінних, які видні з того чи іншого місця в коді. Чимось scope схоже на сам lexical environment, але знову ж таки: lexical environment — це суто джаваскриптовий цілком реальний об’єкт в пам’яті. А scope — це теоретичний термін, який має на увазі саме сукупність доступних змінних і використовується не лише в js.
Три нюанси
Спрощення
Ми тут розглянули лише частину значень, які береже в собі функція. Так то їх ще є. Environment Record теж має в собі й інші значення які ми тут не розглядали. І взагалі всі ті значення невидимі (вони «під капотом») і звісно ж в пам’яті пристрою вони бережуться НЕ в JSON форматі. Але для цілей статті було взято до уваги лише те, що важливо в цій темі і подано не дослівно як в офіційній сухій специфікації, а так щоб це легше було прочитати і уявити.
Це ніби очевидно, але про всяк випадок нагадаю: поданий тут матеріал — це не офіційна специфікація, а спрощена подача.
new Function
Вище я на це не акцентував увагу. І може скластись враження, що const f = new Function() і const f = function() {} — це повністю те ж саме.
Але все трохи складніше. Навіть якщо заморочишся і доб’єшся щоб браузер дозволив виконати new Function(), всеодно буде різниця. Коли рушій створює функцію через new Function(), він в її Reference to the Outer Lexical Environment зберігає посилання на цілий глобальний lexical environment всього документа, а не на lexical environment найближчого блока коду.
Але знову ж таки, ще раз нагадаю, навіть якщо обійдеш обмеження безпеки — всеодно створювати функції через new Function() — це погана практика і так робити не треба.
function declaration vs function expression; var vs let & const; function expression vs function declaration; sloppy vs strict mode
Це тема прям для душних задротів особливо витривалих і допитливих.
Якщо голова і так кипить, скіпни або вертайся потім.
Серйозно. Якщо голова і так кипить — цей розділ пропускай.
Психічно врівноважені люди в 2026 році такого не використовують і навіть на співбесідах такого не питають (а з неврівноваженими краще не працювати).
Але якщо збираєшся на співбесіду до динозаврів чи родичів Фредді Крюгера або і справді прям дуже цікаво, то ситуація наступна:
Вище я уже казав, що для кожного блоку коду рушій створює lexical environment; що кожен блок коду має свій lexical environment. Це правда.
Але рушій також створює variable record. Це теж така штука в пам’яті, схожа на lexical environment. Але, увага, variable environment рушій створює лише для цілого документа, для окремого скрипта і для коду функції при запуску, але не створює для простих блоків як от if чи цикли.
І для var рушій використовує не lexical environment, а variable environment. Змінну, яка оголошена через var, рушій зберігає не в найближчий lexical environment (який мають всі блоки), а в найближчий variable environment (якого прості блоки просто не мають). Тому й виглядає ніби змінна «вистрибує» за межі блоку. Хоча вона не «скаче», просто рушій зберігає її до найближчого саме variable environment’у, якого в звичайного блока просто нема.
Тепер що стосується function declaration (function f() {}) vs function expression (const f = function() {}) (став лайк якщо теж постійно путаєш ці назви).
Якщо function expression f = function(){} йде після const або let, то рушій зберігає її в найближчий lexical environment, але якщо йде після var, то в найближчий variable environment. При цьому рушій в усіх випадках всеодно пришиває функції посилання на зовнішній LE, причому пришиває посилання саме на lexical environment того блока, де вона була оголошена. І вийде ось такий прикол:
if (true) {
const hiddenVariable = "Не дивися, я стісняюсь";
var arrowFunctionAfterVar = () => {
console.log(hiddenVariable);
}
var functionExpressionAfterVar = function() {
console.log(hiddenVariable);
}
}
arrowFunctionAfterVar(); // "Не дивився, я стісняюсь"
functionExpressionAfterVar(); // "Не дивився, я стісняюсь"
console.log(hiddenVariable) // ReferenceError: hiddenVariable is not defined
Якщо код запускається в старому режимі (sloppy mode), то function declaration (function f() {}) рушій збереже в variable environment (якого прості блоки не мають). Але при цьому посилання на зовнішнє LE рушій їй сеодно пришиє. Причому пришиє посилання саме на lexical environment того блока, де вона була оголошена. І вийде подібний попередньому прикол:
if (true) {
const closedVariable = "Не дивися, я стісняюсь";
function declaredFunction () {
console.log(closedVariable);
};
}
declaredFunction(); // Не дивися, я стісняюсь
console.log(closedVariable); // ReferenceError: closedVariable is not defined
Хоч сама по собі closedVariable за межами блока недоступна, але функція declaredFunction замкнута на той блок (тобто посилання на зовнішнє LE функції declaredFunction веде на LE блока, а не цілого скрипта) і для коду declaredFunction змінна closedVariable доступна.
Якщо код запускається в новому, суворому strict mode, то змінні, які оголошені після var рушій і далі буде зберігати в найближчий variable environment (хоча адекватні люди в strict mode не використовують var). Але функції, створені через function declaration (function f() {}) в strict mode рушій буде зберігати до lexical environment.
Останній приклад:
// ⚠️ це sloppy mode
if (true) {
var variableAfterVar = "Це значення змінної після var";
function functionDeclaration() {
console.log("Ось це ти побачиш");
};
function declaredSpyFunction() {
console.log(variableAfterConst);
}
const variableAfterConst = "Це значення змінної після const";
const functionExpressionAfterConst = function () {
console.log("Ти цього всеодно не побачиш");
};
}
console.log(variableAfterVar); // ✅ "Це значення змінної після var"
functionDeclaration(); // ✅ "Ось це ти побачиш"
declaredSpyFunction(); // ✅ "Це значення змінної після const"
console.log(variableAfterConst); // ⛔️ variableAfterConst is not defined
functionExpressionAfterConst();
В пам’яті пристрою буде виглядати приблизно ось так:
{
"LEX-ENV-STORAGE": {
"lex-env-0": { // LE цілого скрипта
"Environment_Record": {}, // порожньо, бо всі змінні були оголошені в блоці
"Outer_LE_id": null
},
"lex-env-1": { // LE блока
"Environment_Record": {
"variableAfterConst": "Це значення змінної після const",
"functionExpressionAfterConst": {
"type": "function",
"whatToDo": "console.log(\"Ти цього всеодно не поб...",
"parameters": [],
"reference_to_outer_LE": "lex-env-1",
"reference_to_outer_VE": "var-env-0"
}
},
"Outer_LE_id": "lex-env-0"
},
},
"VAR-ENV-STORAGE": {
"var-env-0": { // VE цілого скрипта
"variableAfterVar": "Це значення змінної після var",
"functionDeclaration": {
"type": "function",
"whatToDo": "console.log(\"Ось це ти побачиш\");",
"parameters": [],
"reference_to_outer_LE": "lex-env-1", // LE саме блока, де була оголошена
"reference_to_outer_VE": "var-env-0" // найближчий VE, якого в блока просто нема, тому VE всього скрипта
},
"declaredSpyFunction": {
"type": "function",
"whatToDo": "console.log(variableAfterConst);",
"parameters": [],
"reference_to_outer_LE": "lex-env-1",
"reference_to_outer_VE": "var-env-0"
},
},
// а для блока VE нема й не буде, бо це простий блок
}
}
І відповідно за межами блока console.log(variableAfterConst); дасть помилку, але все до того буде працювати (але тільки в sloppy mode).
Зверни увагу: Коли викликаємо declaredSpyFunction();, то помилки нема. Функція declaredSpyFunction хоч і бережеться в variable environment, але її reference to the outer LE посилається на lexical environment блока, де вона була оголошена. Тому declaredSpyFunction без проблем отримує доступ до variableAfterConst.
Зверни ще більшу увагу: Якщо ти в моєму прикладі зверху додасиш вгорі "use strict", то помилка вилетить ще на етапі functionDeclaration();, бо в strict mode рушій function declaration теж додає саме в lexical environment, а не в variable environment.
Ще нюанс про strict mode: Якщо ти просто копіюєш код в консоль або підключаєш як простий .js файл і як в старі добрі легко й без заморочок підключаєш до html через <script src="sandbox.js"></script> і явно не вказуєш "use strict", то рушій запускає твій код в режимі sloppy.
Але якщо ти зробиш це в проекті де установлений vite / webpack і напишеш це в файлі, який потім імпортуєш в інше місце, то скоріше всього цей файл буде запущений в режимі strict mode. Бо навіть коли ти явно не вказуєш "use strict", це часто замість тебе робить збирач коду. І навіть якщо збирач коду не вставляє "use strict", але цей файл імпортується в інший файл, то це вже не просто файл, а модуль. А модулі браузер автоматично запускає в strict mode.
І ГОЛОВНЕ: в нормальному продакшні, навіть у відносному legacy (все, що молодше 15 років) — все розбите на окремі файли, все розділено на багато різних окремих компонентиків і функцій які одне одному не заважають, всюди strict mode і все нормально по-людськи. І ніхто не тримає в голові весь цей динозаврячий спадок і не задумується «а що буде якщо внесу зміну на рядку 219380, як це вплине на код на рядку 182738». Я теж хоч зараз поки пишу статтю пригадав ті нюанси, але отак щоб в будній день без підготовки просто по пам’яті всіх цих дрібниць не згадаю. Всі ці var vs let, function expression vs function declaration, strict vs sloppy, hoisting — це все прям задротська темна тема чисто для потренувати мозок (я попереджав, що краще скіпати) . Ці особливості використовують або при розробці справді унікальних технічних рішень (які зазвичай виявляються справді унікальним, але говнокодом), або ж на співбесідах. Але на співбесідах коли таке питають, то це або щоб перевірити чи вміє людина адекватно відповісти «чесно кажучи зараз так не згадаю, на практиці ніколи не стикався і в голові не відклалось. А ви справді досі не використовуєте strict mode?», і/або від некомпетентності інтерв’ювера який не зміг спитати нічого кращого, і/або щоб до_тись зробити вигляд ніби кандидат не дотягує і збити його зарплатні очікування, і/або щоб самоствердитись.
Що дійсно важливо — це вшити собі в голову ментальну модель того, що змінні не літають хаотично в хмарках, а цілком організовано бережуться в пам’яті. І що функція — це всього лиш книжка з кодом (який виконує рушій, а не сама функція). І що до функції «пришивається» посилання на зовнішнє LE щоб при запуску коду тієї функції то посилання скопіювалось. От це дійсно допоможе розвіяти магічний туман невідомості і краще дебажити код і розуміти де що бережеться і в який момент часу в якої змінної яке значення.
А от всі ті deprecated штуки по типу var, sloppy mode і т д — достатньо просто знати, що воно існує. Але не більше.
Закінчення
Сподіваюсь зміг не лише запутати, а й розпутати.
Ну і якщо було корисно або залишились питання, то став лайк і пиши комент. Якщо я побачу, що на dou людям цікаві не лише срачі про зарплати і мобілізацію, а й технічні статті, то постараюсь розкрити деякі інші теми.
Допобачення
5 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарівСправді гарно написано, доступно наскільки дозволяє тема. Окремо сподобалося — вставки, додають інтерактиву та живості))
Можливо я вас здивую, але в поточних реаліях знання всіх оцих «var vs let, function expression vs function declaration, strict vs sloppy, hoisting» вимагають на етапі відбору до неоплачуваних3-6 місячних навчальних програм.
це ніяк не відміняє сказаного ))
бо в реальній роботі var і sloppy mode досі використовують лише
Коли хочеш здатись дуже вумним питай всяку базову дікуху у людей, які в коді більше 5 років не бачили var. І кричи: «ти ж повинен знати, тиж сеньйор...». Для цих «спеціалістів» окремий котел в пеклі.
Я собі завжди уявляв зв’язок між lexical envs немов вони зв’язані через ланцюг прототипів.
Може воно і не так, але така модель допомагає легше зрозуміти як саме відбувається пошук.