З мого досвіду викладання і після опитувань студентів я зрозумів, що контекст виконання - це та тема, яка дуже часто “ну ніяк не йде”. Студентам важко осягнути щось таке абстрактне, те, що неможливо побачити, зрозуміти що коли і навіщо в ньому створюється.
Тож я тут для того, щоб по-класиці спробувати донести до вас цю непросту на перший погляд штуку. Поїхали :)
Контекст виконання в JavaScript - це, мабуть, та сама найважливіша річ, яку ви повинні зрозуміти, оскільки чітке розуміння цього дасть вам ті базові знання, які необхідні для розуміння більш складних концепцій, таких як hoisting (підняття) і closure (замикання), бо без розуміння цього ви просто не зможете працювати.
Одразу обмовлюся, що стаття пояснює як це працює в межах мови, а не рушія. Основні принципи і там, і там однакові (або хоча б схожі, в залежності від рушія), а реалізація може відрізнятися в різних рушіях.
Що ж таке контекст виконання?
Дуже спрощено, контекст виконання - це середовище, в якому працює наш код.
Чим глибше ми будемо розбиратися в цій темі, тим більше ви будете розуміти, що таке оточення (той самий контекст). Зараз же ви можете уявляти його як коробку, де “лежить” весь наш код.
В JavaScript у нас є різні типи коду, а саме:
- код, який знаходиться в глобальному контексті;
- код, який знаходиться всередині контексту функції;
- також є код, який знаходиться всередині функції eval.
Кожного разу, коли ваша програма викликає функцію, створюється новий контекст виконання. У рекурсивних функціях кожного разу, коли функція викликає сама себе, створюється новий контекст виконання, тому теоретично ви можете мати нескінченну кількість контекстів виконання. І цього абсолютно не варто боятися.
Коротше кажучи, ви можете мати 3 різних типи контексту виконання:
1. Глобальний контекст виконання:
До цього відноситься весь код, який не знаходиться всередині функції, тож він знаходиться у глобальному оточенні.
Глобальне оточення може бути тільки одне, і глобальне оточення також містить глобальний об'єкт (вікно у браузері), значення якого у нестрогому режимі дорівнює глобальному оточенню.
let globalVar = "Це глобальна змінна";
const showGlobalVar = () => {
console.log(globalVar);
};
showGlobalVar(); // Виводить: Це глобальна змінна
2. Контекст виконання функції:
Кожного разу, коли виконується функція, для неї створюється новий контекст виконання. Таким чином, кожна функція має власний контекст виконання, який створюється, коли код викликає функцію, і точно не раніше.
const outerFunction = () => {
let outerVar = 'Я зовнішня змінна';
const innerFunction = () => {
let innerVar = 'Я внутрішня змінна';
console.log(outerVar); // Виводить: Я зовнішня змінна
console.log(innerVar); // Виводить: Я внутрішня змінна
};
innerFunction();
};
outerFunction();
3. Контекст виконання функції eval:
Він створюється при виклику функції eval, але оскільки більшість розробників не використовують eval, ми не будемо говорити про нього у цій статті.
const globalVariable = "Це глобальна змінна";
const evalExample = () => {
eval('var evalVar = "Це змінна, створена в eval"; console.log(evalVar); console.log(globalVariable);');
};
evalExample();
// Вивід у консолі:
// Це змінна, створена в eval
// Це глобальна змінна
Стек виконання (стек викликів) – це структура даних стека, яка використовується, як колекція всіх контекстів виконання, що є активними під час роботи коду.
Стек працює за принципом LIFO (Last in, first out). Це означає, що останній елемент, який потрапляє до стеку, також є першим, який виходить зі стеку.
Для більш простого пояснення прикріплю зображення, за допомогою якого зрозуміти буде дуже просто:
Розберемося, як же працює стек
Тепер, коли ми маємо базове уявлення про те, як виглядає стек, ми можемо розглянути, як він працює.
Загалом, кожен контекст можна розглядати як відправника або отримувача функцій. Наприклад, якщо одна функція викликає іншу, контекст цього виклику є відправником, а контекст функції, яка викликається, - отримувачем.
Контекст може бути одночасно і відправником, і отримувачем, наприклад, функція, яка викликається з глобального контексту, а потім викликає іншу функцію. Тут вже стає складніше для розуміння.
Коли так званий відправник викликає функцію, він зупиняє її виконання і фактично передає потік керування отримувачу. У цей момент виклик переміщується до контексту виконання і стає активним контекстом виконання.
Як тільки код в активному контексті виконання завершує роботу, потік керування повертається назад до відправника, і функція продовжує виконуватися.
Тобто наступний код:
function firstFunc() {
console.log('Executing first function')
secondFunc();
}
function secondFunc() {
console.log('Executing second function')
}
firstFunc();
Буде виглядати наступним чином:
Як ви можете зрозуміти, стек викликів працює синхронно, активний стек завжди представляє контекст, який є активним в даний момент, в тому порядку, в якому він був викликаний.
На цьому ми рухаємось далі.
Рекомендуємо курс по темі
Як виглядає процес створення контексту виконання?
Тепер, коли ми розуміємо, що таке контекст виконання і як працює стек викликів, залишається декілька важливих питань. Навіщо взагалі потрібен контекст виконання? За що він відповідає? І що містить він містить в собі?
Кожен раз створення контексту виконання відбувається у два етапи:
- Фаза створення;
- Фаза виконання.
Фаза створення починається після створення контексту виконання, але до того, як код буде запущено. Візьмемо для прикладу виклик функції.
Коли ви викликаєте функцію, ви можете подумати, що код негайно виконується, але насправді починається фаза створення, хоча код насправді не виконується. Є деякі речі, які відбуваються до того, як він буде виконаний.
Я уявляв це собі як процес формування заготовок. Під час створення (першої фази) ми створюємо заготовку, а на етапі виконання ця заготовка заповнюється конкретною потрібною інформацією.
Що я маю на увазі під заготовкою?
На етапі створення заготовки рушій переглядає код, і щоразу, коли він зустрічає оголошення змінної або функції, він зберігає змінні без їх фактичних значень, за винятком аргументів функції, де значення зберігаються.
Потім, на етапі виконання, рушій прогортає цю заготовку і виконує кожну відповідну частину.
Цей процес повторюється кожен раз, коли створюється новий контекст виконання, рушій створює шаблон оголошень змінних і функцій, і лише потім на етапі виконання присвоює значення змінним і фактично виконує код. Це і була фаза виконання.
Компоненти контексту виконання
Технічно, контекст виконання містить наступні елементи:
ExecutionContext = {
ThisBinding: привʼязка this,
VariableEnvironment: { ... },
LexicalEnvironment: { ... }
}
Всі ці елементи створюються на першому етапі створення, і кожен з них виконує свою роль. Наприклад, середовище змінних - це те, що насправді містить змінні та їхні значення.
Але глибше в тему компонентів контексту виконання ми занурюватися зараз не будемо, хоч кожен з компонентів - це абсолютно невідʼємна частина контексту виконання. Для того, щоб щось зрозуміти - це потрібно “переварити”, бо так краще засвоюється :) А про компоненти - ми можемо поговорити в майбутньому.
Висновок
Є 3 типи контексту виконання: глобальний контекст виконання, функціональний контекст виконання, контекст виконання eval.
Кожен контекст виконання керується стеком виконання у вигляді відправника та отримувача. Ми дізналися, що існує дві фази, які відбуваються кожного разу, коли створюється контекст виконання: фаза створення і фаза виконання.
На етапі створення створюється певна заготовка змінних і функцій.
На етапі виконання рушій переглядає код, виконує присвоювання змінних і виконує код.
Ми також згадували, що технічно контекст виконання містить такі речі, як: LexicalEnvironment, VariableEnvironment і thisBinding. Оскільки кожна з них є окремою темою, ми продовжимо говорити про них у наступних статтях.
Тож до зустрічі!