Стек вызовов JavaScript: объяснение с помощью иллюстраций

Если коротко, стек вызовов  —  это стек, который управляет порядком выполнения функций. Другими словами, это механизм, который контролирует, какая функция будет выполнена и когда это произойдет.

Стек представляет собой структуру данных, организованных по принципу LIFO, т.е. в порядке обратной очередности (last-in  —  first-out, последним вошел  —  первым вышел). Это означает, что первой будет выполняться последняя функция, внесенная в стек вызовов.

Как функция попадает в стек вызовов?

Когда код оценивается, каждая выполненная функция попадает в стек вызовов. Другой интересный вопрос: когда функция выходит из стека вызовов? Это произойдет только после завершения выполнения всех функций, которые были вызваны ею (если таковые были), и после того, как все строки кода внутри нее будут оценены.

Типичный случай

В случае ниже каждая функция завершается после выполнения всех строк кода внутри нее.

function defense() {
console.log('I am the defense attorney');
}

function prosecution() {
console.log('I am the prosecutor');
}

console.log('All rise');
defense();
prosecution();

Более интересный случай

В данном примере  —  вызов функции внутри другой функции.

function callingToWitness(witnessNumber) {
console.log('Calling to witness number ' + witnessNumber);
}

function defense() {
callingToWitness(1);
callingToWitness(2);
}

function prosecution() {
callingToWitness(3);
callingToWitness(4);
}

console.log('All rise');
defense();
prosecution();

Вот что мы увидим в стеке вызовов в этом случае:

Этот пример помогает лучше понять термин “стек”. Благодаря вызову функции внутри другой функции мы видим, как “боксы” нагромождаются один на другой и создают стек “боксов”.

Каждый “бокс” выполняет код внутри себя. При вызове функции создается новый “бокс” внутри нее. Если есть простая команда, например console.log, она просто выполняется. Выполнив весь код, “бокс” покидает стек.

Что такое “боксы”?

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

Пример этого показан в GIF-изображении выше. Функция callingToWitness получает параметр witnessNumber, который хранится в ее контексте выполнения. Мы видим, что эта функция выполняется несколько раз, но ее контекст каждый раз разный.

Зачем нужны контексты выполнения?

Посмотрим, что произойдет, если попытаться вывести член, который не объявлен в контексте выполнения.

function prosecution() {
  whoAmI = 'The Prosecutor';
  prosecutorName = 'Adam';
	
  console.log("Who am I: " + whoAmI);
  
  callWitness();
}

function callWitness() {
  whoAmI = 'The Witness';
  
  console.log("Who am I: " + whoAmI);
  console.log("The prosecutor's name: " + prosecutorName);
  console.log("The judge's name: " + judgeName);
}

console.log("All rise");

judgeName = 'Emma'
whoAmI = 'The judge';

prosecution();

Итак, первый контекст выполнения  —  это общий контекст выполнения (General Execution Context). В нем хранится весь код, который находится за пределами объявленных функций. Внутри него мы видим контексты выполнения для каждого выполнения функции.

В приведенном выше примере видно, что во время выполнения функции callWitness ее контекст выполнения не содержит членов с именами judgeName и prosecutorName, но один из контекстов выполнения ее “предков” содержит эти члены.

Попробуем получше разобраться в выполнении функции CallWitness.

  • Вывод whoAmI. Этот параметр объявляется в области выполнения функции и в каждом из контекстов выполнения ее предков. Он будет считан из контекста выполнения функции. Всегда нужно искать ближайший контекст выполнения, в котором объявлен член. Очевидно, что в данном случае это контекст выполнения текущей функции.
  • Вывод prosecutorName. Этот параметр не был объявлен в самой функции. Мы видим, что он был объявлен в функции prosecution. Это контекст выполнения, в котором выполнялась функция, и поэтому функция может читать ее члены.
  • Вывод judgeName. Этот параметр также не был объявлен в самой функции. Он был объявлен в общем контексте выполнения, который является одним из предков функции, поэтому она может читать его члены.

Таким образом, стек вызовов напоминает скорее матрешку, чем простой стек. Контексты выполнения открываются друг в друге и имеют контекст контекстов выполнения своих предков в дополнение к своему собственному.

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Hila Kraisler Cohen: Understanding the JavaScript Call Stack Through Illustrations

Предыдущая статьяКак создать чат-бот на основе данных CSV с LangChain и OpenAI
Следующая статьяРеактивное программирование с Combine