Що таке контекст виконання (execution context)?
Execution context (контекст виконання) - внутрішнє середовище, яке JavaScript-рушій створює щоразу при запуску коду. В ньому зберігаються змінні, ланцюжок областей видимості та прив'язка this.
Теорія
TL;DR
- Аналогія: кожен виклик функції отримує власне робоче місце зі своїм набором змінних, довідником (ланцюжок областей видимості) і тегом власника (
this). - Три типи: глобальний (один на всю програму), функціональний (окремий на кожен виклик), eval (краще не чіпати).
- Дві фази: фаза створення (тут відбувається hoisting) і фаза виконання (код виконується рядок за рядком).
thisзмінюється залежно від контексту. Стрілкові функції власного контексту не створюють, вони берутьthisвід батька.- Отримав несподіваний
undefinedабо неправильнийthis- трасуй стек контекстів.
Швидкий приклад
console.log(a); // undefined - підняте у фазі створення глобального контексту
var a = 2;
console.log(a); // 2
function greet() {
// Новий функціональний контекст кладеться на стек викликів
console.log(b); // undefined - підняте всередині цього контексту
var b = 'hello';
console.log(b); // 'hello'
}
greet();Глобальний контекст піднімає a до undefined під час фази створення. Коли викликається greet(), на стек кладеться новий функціональний контекст зі своїм b. Він бачить a через ланцюжок областей видимості, але b існує тільки всередині нього.
Дві фази кожного контексту
Рушій виконує кожен контекст у два кроки.
Фаза створення: змінні з var підіймаються і отримують undefined. Оголошення функцій підіймаються повністю. Будується ланцюжок областей видимості. Визначається this.
Фаза виконання: код іде рядок за рядком, змінним присвоюються значення, функції викликаються.
Саме тому звернення до x перед var x = 1 дає undefined, а не ReferenceError. Змінна вже зареєстрована в контексті під час фази створення, просто без значення.
Чим execution context відрізняється від scope
Область видимості (scope) визначає, які змінні бачить конкретна ділянка коду. Execution context - це повний пакет: об'єкт змінних (всі var і оголошення функцій), ланцюжок областей видимості до зовнішніх контекстів, значення this і позиція в стеку викликів. Scope - лише один компонент контексту.
this у різних контекстах
При виклику методу this - це об'єкт перед крапкою. При звичайному виклику функції this - глобальний об'єкт (або undefined в strict mode). Стрілкові функції власного контексту не створюють. Вони захоплюють this з оточуючого контексту в момент оголошення.
const obj = {
name: 'World',
hello: function() {
console.log(this.name); // 'World' - контекст методу, this = obj
setTimeout(function() {
console.log(this.name); // undefined - новий глобальний контекст
}, 0);
setTimeout(() => {
console.log(this.name); // 'World' - стрілка успадковує this з hello
}, 0);
}
};
obj.hello();Звичайна функція в setTimeout отримує власний глобальний контекст. Стрілкова - взагалі не створює нового.
Типові помилки
1. Очікувати що var обмежений блоком
if (true) {
var score = 100;
}
console.log(score); // 100 - var належить функціональному або глобальному контекстуvar прив'язаний до свого execution context, а не до блоку {}. Для блокової видимості використовуй let або const.
2. Втрата this у callback
class Timer {
constructor() { this.count = 0; }
start() {
setInterval(function() {
this.count++; // TypeError: this is undefined в strict mode
}, 1000);
}
}Це, мабуть, найчастіше питання про this на junior-співбесідах з JavaScript. Callback виконується в новому контексті, де this не є екземпляром Timer. Рішення: стрілкова функція в callback або .bind(this) у конструкторі.
3. Очікувати що eval ізольований
var secret = 'admin';
eval('console.log(secret)'); // 'admin' - успадковує зовнішній scopeeval створює власний контекст, але успадковує зовнішню область видимості. Плюс вимикає частину оптимізацій V8. Для ізольованого динамічного коду використовуй new Function().
4. Забути що strict mode змінює this
Без strict mode this у звичайному виклику функції - глобальний об'єкт. У strict mode - undefined. Додавання "use strict" до існуючого файлу може непомітно зламати код, який розраховував на this === window.
Де це зустрічається на практиці
- React function components: кожен рендер створює новий функціональний контекст. Хуки на кшталт
useStateзахоплюють стан для цього контексту через замикання (closure). - Express route handlers: кожен запит виконується у власному контексті.
reqіresлокальні для цього виклику. - Callbacks у
setTimeout: виконуються в новому глобальному контексті. Стрілкові функції - стандартне рішення для збереженняthis. - Node.js модулі: кожен файл запускається у wrapper-функції зі своїм контекстом, тому
varна верхньому рівні не потрапляє в глобальний об'єкт.
Follow-up питання
Q: Яка різниця між фазою створення і фазою виконання?
A: У фазі створення var-змінні підіймаються до undefined, оголошення функцій підіймаються повністю, визначається this. У фазі виконання код іде рядок за рядком і змінним присвоюються значення.
Q: Як ланцюжок областей видимості працює між контекстами?
A: Кожен контекст зберігає посилання на VariableEnvironment зовнішнього контексту. Якщо змінна не знайдена локально, рушій іде вверх по ланцюжку до глобального контексту.
Q: Яка різниця між call stack і стеком execution context?
A: Це один механізм з двох точок зору. Call stack відстежує які функції чекають повернення. Стек execution context описує повний стан кожного фрейму: змінні, ланцюжок областей видимості і this.
Q: Як стрілкові функції впливають на execution context?
A: Вони не створюють власного контексту. this і scope успадковуються від лексичного батька в момент оголошення функції.
Q: Чому this є undefined у Node.js strict mode всередині функції?
A: Strict mode не прив'язує глобальний об'єкт до this при звичайному виклику функції. Значення залишається undefined поки не буде встановлене явно через .call(), .apply() або .bind().
Приклади
Hoisting у глобальному і функціональному контекстах
// Глобальний контекст - 'name' піднімається до undefined у фазі створення
console.log(name); // undefined
var name = 'Alice';
console.log(name); // 'Alice'
function getGreeting() {
// Функціональний контекст - власна фаза створення
console.log(greeting); // undefined (підняте в цьому контексті)
var greeting = 'Hello, ' + name; // 'name' знайдено через ланцюжок
console.log(greeting); // 'Hello, Alice'
}
getGreeting();name доступна всередині getGreeting через ланцюжок областей видимості до глобального контексту. greeting існує тільки у власному контексті функції і не видима зовні.
this у React-компоненті
function UserProfile({ userId }) {
// Новий execution context при кожному рендері
const [user, setUser] = React.useState(null);
React.useEffect(() => {
// Стрілкова функція - власного контексту немає, успадковує від UserProfile
fetch(`/api/user/${userId}`) // userId з контексту цього рендеру
.then(res => res.json())
.then(setUser);
}, [userId]);
return user ? <div>{user.name}</div> : <div>Loading...</div>;
}Кожен рендер UserProfile створює новий execution context. Стрілкова функція в useEffect замикає userId з конкретного рендеру. Якщо не вказати userId в масиві залежностей, ефект триматиме застаріле значення зі старого контексту - це класичний баг із stale closure.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.