Пошук уроків, статей та іншого контенту
Сформуйте цілісне уявлення про те, що відбувається від моменту запуску JavaScript-коду до виконання окремих функцій. Познайомтеся з Execution Context, Call Stack і областями видимості як частинами єдиної моделі виконання.
Коли JavaScript запускає програму, він не виконує весь файл як один нероздільний блок. Для кожного глобального коду, виклику функції або виконання модуля створюється контекст виконання (Execution Context).
Контекст виконання містить інформацію, потрібну рушію JavaScript для виконання коду:
змінні та функції, доступні в поточній області видимості;
значення this;
посилання на зовнішню область видимості;
поточну позицію виконання коду.
У спрощеному вигляді процес можна описати так:
рушій створює контекст виконання;
готує змінні, функції та області видимості;
виконує інструкції зверху вниз;
під час виклику функції створює новий контекст;
після завершення функції видаляє її контекст зі стеку викликів.
Основними частинами цієї моделі є:
Execution Context — середовище виконання конкретного коду;
Call Stack — стек активних викликів;
Scope — правила доступу до змінних.
Після запуску скрипту JavaScript створює глобальний контекст виконання. Він існує протягом усього часу роботи програми.
const applicationName = "Task Manager";
function showApplicationName() {
console.log(applicationName);
}
showApplicationName();Спочатку створюється глобальний контекст. У ньому доступні:
applicationName;
функція showApplicationName;
глобальні об'єкти та функції, наприклад console.
Потім виконується виклик showApplicationName(). Для нього створюється окремий контекст функції.
Глобальний контекст не зникає після завершення функції, оскільки глобальний код усе ще є активним.
Кожен виклик функції отримує власний контекст виконання.
function multiply(a, b) {
const result = a * b;
return result;
}
const value = multiply(4, 5);
console.log(value);Під час виклику multiply(4, 5) створюється контекст, у якому є:
параметр a зі значенням 4;
параметр b зі значенням 5;
локальна змінна result;
доступ до зовнішньої області видимості.
Після виконання return result контекст функції стає непотрібним і прибирається зі стеку викликів.
Важливо розрізняти функцію як значення та виклик функції:
function greet() {
console.log("Привіт");
}
// Оголошення функції не створює контекст її виконання.
const savedFunction = greet;
// Контекст створюється саме під час виклику.
savedFunction();У середовищах, які підтримують JavaScript-модулі, кожен файл із модульним кодом має власну область видимості. Змінні такого файлу не стають автоматично глобальними.
// module.js
const internalValue = 42;
export function getValue() {
return internalValue;
}internalValue доступна всередині модуля, але не в інших файлах, якщо її явно не експортувати.
У браузері модульний скрипт запускають із type="module":
<script type="module" src="module.js"></script>Для розуміння основної моделі виконання достатньо пам'ятати: глобальний код і код модуля мають власний контекст, а кожен виклик функції створює новий функціональний контекст.
Створення контексту можна умовно поділити на дві фази:
фаза створення;
фаза виконання.
Це не два окремі проходи, доступні програмісту як API. Це модель, яка допомагає пояснити поведінку JavaScript.
Під час створення контексту рушій готує:
області видимості;
параметри функції;
оголошення функцій;
змінні та константи;
значення this;
посилання на зовнішній контекст.
Розглянемо приклад:
console.log(message);
sayHello();
var message = "Привіт";
function sayHello() {
console.log("Вітаю!");
}Результат:
undefined
Вітаю!Оголошення функції sayHello доступне до рядка, де вона записана, тому що оголошення функцій обробляються під час створення контексту.
Змінна message, оголошена через var, також відома контексту до виконання рядка присвоєння, але спочатку має значення undefined.
У спрощеній формі рушій поводиться приблизно так:
var message = undefined;
function sayHello() {
console.log("Вітаю!");
}
console.log(message);
sayHello();
message = "Привіт";Це не означає, що JavaScript буквально переписує код у такому вигляді. Це лише модель для пояснення підняття оголошень.
let і const та тимчасова мертва зонаЗмінні, оголошені через let і const, також враховуються під час створення контексту, але до моменту фактичного виконання оголошення вони перебувають у тимчасовій мертвій зоні (Temporal Dead Zone, TDZ).
console.log(value); // ReferenceError
const value = 10;На відміну від var, змінна value не отримує доступного значення undefined. Спроба звернутися до неї до оголошення спричиняє ReferenceError.
Те саме стосується let:
{
console.log(count); // ReferenceError
let count = 3;
}Практичне правило: оголошуйте змінні до їх використання. Це робить порядок виконання очевидним і не залежить від деталей підняття.
Call Stack — це структура даних, яка зберігає активні контексти виконання.
Стек працює за принципом LIFO (Last In, First Out):
останній доданий елемент буде видалений першим;
новий виклик функції додається на вершину;
після завершення функції її контекст знімається з вершини.
Розглянемо послідовність:
function first() {
second();
}
function second() {
third();
}
function third() {
console.log("Виконання third");
}
first();Під час роботи стек змінюється так:
глобальний код додається до стеку;
викликається first — додається контекст first;
first викликає second — додається контекст second;
second викликає third — додається контекст third;
third завершується і видаляється;
завершується second;
завершується first;
продовжується глобальний код.
У спрощеній формі:
global
global → first
global → first → second
global → first → second → third
global → first → second
global → first
globalПоки верхній контекст не завершився, JavaScript не переходить до контекстів нижче нього.
Звичайний JavaScript-код виконується синхронно: наступна інструкція починає виконуватися після завершення попередньої.
console.log("Початок");
function processData() {
console.log("Обробка даних");
}
processData();
console.log("Кінець");Результат:
Початок
Обробка даних
КінецьВиклик processData() повністю завершується до виконання наступного console.log.
Якщо функція викликає саму себе без умови завершення, стек постійно зростає:
function repeat() {
repeat();
}
repeat();Зрештою рушій зупинить програму з помилкою на кшталт RangeError: Maximum call stack size exceeded.
Рекурсія є корисною, але вона повинна мати базовий випадок:
function countdown(number) {
if (number === 0) {
console.log("Старт!");
return;
}
console.log(number);
countdown(number - 1);
}
countdown(3);Тут виклики завершуються, коли number дорівнює 0.
Область видимості (scope) визначає, де змінну можна прочитати або змінити.
JavaScript використовує лексичну область видимості: доступ до змінних визначається структурою коду, а не місцем виклику функції.
Основні види областей видимості:
глобальна;
функціональна;
блочна;
модульна.
Змінна, оголошена на верхньому рівні скрипту, доступна вкладеному коду, якщо її не перекриває інша змінна з таким самим іменем.
const language = "JavaScript";
function printLanguage() {
console.log(language);
}
printLanguage();Функція не має локальної змінної language, тому JavaScript шукає її у зовнішній області та знаходить глобальну змінну.
Змінні, оголошені через var, мають функціональну область видимості:
function createMessage() {
var message = "Внутрішнє повідомлення";
console.log(message);
}
createMessage();
// console.log(message); // ReferenceErrormessage доступна всередині createMessage, але не за її межами.
Змінні, оголошені через let і const, мають блочну область видимості. Блоком є код усередині {}:
if (true) {
const status = "активний";
let count = 1;
console.log(status, count);
}
// status і count тут недоступніБлочна область допомагає обмежувати життєвий простір змінних і зменшує ризик випадкових конфліктів імен.
Якщо змінну не знайдено в поточній області, JavaScript перевіряє зовнішню область. Потім — область за нею, і так далі до глобальної.
const application = "CRM";
function createReport() {
const format = "PDF";
function printReport() {
const title = "Місячний звіт";
console.log(application);
console.log(format);
console.log(title);
}
printReport();
}
createReport();Для printReport:
title знаходиться в поточній області;
format — у зовнішній області createReport;
application — у глобальній області.
Якщо змінну не буде знайдено в усьому ланцюжку, виникне ReferenceError.
function showValue() {
console.log(unknownValue);
}
showValue(); // ReferenceErrorПошук відбувається від внутрішньої області до зовнішньої, але не навпаки. Зовнішній код не може звернутися до локальних змінних вкладеної функції.
Замикання (closure) виникає, коли функція зберігає доступ до змінних своєї зовнішньої області навіть після завершення зовнішньої функції.
function createCounter() {
let count = 0;
return function increment() {
count += 1;
return count;
};
}
const counter = createCounter();
console.log(counter()); // 1
console.log(counter()); // 2
console.log(counter()); // 3Після завершення createCounter її контекст більше не перебуває у Call Stack. Проте внутрішня функція increment усе ще посилається на count.
Тому рушій зберігає необхідне лексичне оточення, доки воно потрібне замиканню.
Замикання корисні для:
приховування внутрішнього стану;
створення функцій із налаштованою поведінкою;
реалізації фабрик і модулів;
обробників подій та асинхронних операцій.
Контекст виконання і область видимості — пов'язані, але не тотожні поняття.
Контекст виконання відповідає на питання:
Який код зараз виконується і які службові дані потрібні для цього виконання?
Область видимості відповідає на питання:
До яких змінних має доступ цей код?
Наприклад, під час виклику функції створюється новий контекст. У ньому є локальні параметри та змінні, а також посилання на зовнішню лексичну область:
const taxRate = 0.2;
function calculatePrice(price) {
const tax = price * taxRate;
return price + tax;
}
console.log(calculatePrice(100));Контекст calculatePrice містить параметр price і локальну змінну tax. Через зовнішню область функція отримує доступ до taxRate.
this у контексті виконанняЗначення this є частиною контексту виконання, але воно визначається не лексичним ланцюжком змінних, а способом виклику функції.
const user = {
name: "Олена",
showName() {
console.log(this.name);
}
};
user.showName(); // ОленаУ виклику user.showName() значення this дорівнює user.
Стрілкові функції не створюють власного this. Вони використовують this із зовнішнього контексту:
const user = {
name: "Олена",
createMessage() {
const getMessage = () => {
return `Користувач: ${this.name}`;
};
return getMessage();
}
};
console.log(user.createMessage());Не варто змішувати правила пошуку змінних із правилами визначення this. Змінна шукається в лексичному ланцюжку, а this у звичайній функції залежить від форми виклику.
Call Stack не виконує довгі операції паралельно. Асинхронні API середовища, наприклад таймери, можуть виконувати очікування поза стеком, а їхні callback-функції будуть заплановані для виконання пізніше.
console.log("Початок");
setTimeout(() => {
console.log("Таймер");
}, 0);
console.log("Кінець");Результат:
Початок
Кінець
ТаймерНавіть із затримкою 0 callback не виконується негайно. Спочатку завершується поточний синхронний код. Після цього середовище JavaScript може передати callback на виконання.
У браузері за цей процес відповідають не лише механізми самого рушія JavaScript, а й середовище виконання:
Call Stack виконує поточний код;
Web API обробляють операції на кшталт таймерів;
черги зберігають готові callback-функції;
event loop координує їх додавання до стеку, коли стек вільний.
Це пояснює, чому асинхронний callback не перериває виконання поточного синхронного коду.
const basePrice = 100;
function createPriceCalculator(discount) {
return function calculatePrice() {
const discountedPrice = basePrice * (1 - discount);
return discountedPrice;
};
}
function printPrice() {
const calculatePrice = createPriceCalculator(0.1);
console.log(calculatePrice());
}
console.log("Початок");
printPrice();
console.log("Кінець");Послідовність виконання:
створюється глобальний контекст;
у ньому готуються basePrice, createPriceCalculator і printPrice;
виконується console.log("Початок");
викликається printPrice, тому до стеку додається її контекст;
усередині printPrice викликається createPriceCalculator;
createPriceCalculator створює внутрішню функцію calculatePrice і повертає її;
контекст createPriceCalculator завершується;
printPrice викликає calculatePrice;
calculatePrice знаходить discount у замиканні, а basePrice — у глобальній області;
calculatePrice повертає результат і завершується;
завершується printPrice;
виконується console.log("Кінець").
Цей приклад одночасно демонструє:
глобальний і функціональні контексти;
Call Stack;
лексичний ланцюжок;
замикання;
повернення значення з функції.
function logMessage() {
console.log("Повідомлення");
}Цей код лише створює функцію. Її тіло виконається після виклику:
logMessage();Область видимості визначає доступ до змінних. Call Stack показує, які функції зараз виконуються. Вкладена функція може мати доступ до змінної зовнішньої області навіть після того, як зовнішній виклик зник зі стеку — це випадок замикання.
setTimeout(..., 0) виконається одразуНульова затримка не означає негайне виконання. Callback буде виконаний після завершення поточного синхронного коду.
var без розуміння його областіvar має функціональну, а не блочну область видимості:
if (true) {
var value = "доступне";
}
console.log(value); // доступнеУ сучасному коді зазвичай використовують const, а let — коли значення потрібно змінювати.
Кожен рекурсивний виклик додає новий контекст до Call Stack. Без базового випадку це призводить до переповнення стеку.
this шукається як звичайна зміннаПравила this відрізняються від правил лексичної області видимості. Для звичайної функції важливий спосіб виклику, а стрілкова функція успадковує this із зовнішнього контексту.
JavaScript виконує код у контекстах виконання.
Глобальний код отримує глобальний контекст.
Кожен виклик функції створює новий функціональний контекст.
Call Stack зберігає контексти активних викликів за принципом LIFO.
Під час створення контексту рушій готує змінні, функції, області видимості та this.
var має функціональну область, а let і const — блочну.
Область видимості визначає, де код може отримати доступ до змінної.
JavaScript шукає змінні через лексичний ланцюжок — від поточної області до зовнішніх.
Замикання дають функції змогу зберігати доступ до зовнішніх змінних.
Синхронний код виконується через Call Stack послідовно.
Асинхронні callback-функції виконуються пізніше, коли поточний стек звільняється.