Execution context и lexical environment часто звучат как абстрактные термины из спецификации. Из-за этого вокруг них легко появляется лишняя мистика, хотя на практике они описывают довольно простые вещи: где код сейчас выполняется и какие переменные ему доступны.

Без этих двух идей сложно до конца понять scope, this, стек вызовов и замыкания. Поэтому имеет смысл не заучивать определения, а связать их с обычным кодом, который мы пишем каждый день.

Разберем, что именно означает контекст выполнения, чем он отличается от лексической среды и как это влияет на поведение функций.

Что такое execution context

Контекст выполнения - это среда, в которой движок JavaScript выполняет текущий код.

Когда запускается скрипт, JavaScript создает глобальный контекст выполнения. Когда вызывается функция, для нее создается отдельный контекст выполнения. Когда функция заканчивает работу, ее контекст удаляется из стека.

Упрощенно можно думать о контексте как о пакете информации, который нужен движку прямо сейчас:

  • где мы находимся в коде;
  • какие локальные переменные доступны;
  • на что указывает this;
  • какой внешний scope нужно использовать при поиске переменных.

В браузере в глобальном скрипте globalThis указывает на объект window:

console.log(globalThis === window); // true

В Node.js глобальный объект другой, но принцип тот же: у глобального кода есть собственный контекст выполнения.

Как контексты попадают в стек вызовов

JavaScript однопоточен: в один момент времени выполняется только один контекст. Остальные ждут своей очереди в call stack.

function printName() {
  return "Alex";
}

function sayName() {
  return printName();
}

console.log(sayName());

Порядок здесь такой:

  1. Создается глобальный execution context.
  2. Вызывается sayName() и попадает в стек.
  3. Внутри него вызывается printName() и тоже попадает в стек.
  4. printName() завершается и выходит из стека.
  5. sayName() получает результат и тоже завершается.
  6. Выполнение возвращается в глобальный контекст.

Именно поэтому ошибки часто читаются как stack trace: это просто история вложенных execution contexts.

Что такое lexical environment

Лексическая среда отвечает на другой вопрос: где код был объявлен и какие внешние переменные он может видеть.

Это уже не про текущий момент выполнения, а про положение функции в исходном коде.

const language = "JavaScript";

function outer() {
  const topic = "scope";

  function inner() {
    console.log(language);
    console.log(topic);
  }

  inner();
}

outer();

Функция inner объявлена внутри outer, поэтому ее внешняя лексическая среда - это область видимости outer. Из-за этого внутри inner доступны и topic, и глобальная переменная language.

Важно именно место объявления, а не место вызова. Если функцию передать дальше и вызвать в другом месте, ее лексическая среда не изменится.

Почему это важно для scope и closures

Контекст выполнения и лексическая среда работают вместе.

Когда функция начинает выполняться, она получает новый execution context. Но искать переменные она будет по своей лексической цепочке, которая определилась в момент объявления.

Именно так работают замыкания:

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() ее execution context исчезает из стека, но возвращенная функция increment все еще держит ссылку на свою лексическую среду, где находится переменная count.

Поэтому понимать lexical environment полезно не ради теории, а ради объяснения совершенно практичных вещей:

  • почему вложенная функция видит внешние переменные;
  • почему callback сохраняет доступ к локальному состоянию;
  • почему место объявления функции важнее места вызова для поиска переменных.

Итог

Execution context отвечает за текущее выполнение: какой код сейчас запущен, какие локальные данные активны и как устроен стек вызовов. Lexical environment отвечает за область видимости: где функция была объявлена и какие внешние переменные ей доступны.

Если держать в голове эту разницу, проще понимать scope, this, closures и поведение вложенных функций без магии и заученных формулировок из спецификации.