Все мы знаем, что такое переполнение стека. От разработчиков детского уровня до старших разработчиков, мы все ищем ответы на наши коды в переполнении стека. Но что такое «стек» и почему он переполняется?

В этом посте я объясню, что такое стек, в частности, что такое структура данных стека и ее приложения. Кроме того, как языки программирования, такие как JavaScript, используют «стек вызовов» как способ выполнения своего кода. Это звучит как довольно тяжелая тема для освещения, но, поверьте мне, ее довольно легко понять.

Структура данных стека AKA «стек вызовов» — это область памяти, используемая для хранения информации о выполнении программы. Это позволяет программе использовать рекурсию, отслеживая, где она была, чтобы она могла вернуться туда позже. Что это вообще значит? Это просто означает, что стек используется для хранения и организации данных в компьютере путем наложения элементов друг на друга.

Более простой способ представить стек — это связать его с нашей повседневной аналогией. Поскольку стек вызовов не так уж сильно отличается от примера стека реального мира в том, как мы организуем вещи. Когда я думаю о стеке вызовов, я хотел бы думать о нем как о тяжелых обеденных тарелках, которые сложены друг на друга, или о наших старых добрых компакт-дисках.

Давайте посмотрим на примере сложенных компакт-дисков, чтобы мы могли добраться до самого низа стека компакт-дисков, сначала нам нужно удалить то, что уже находится поверх компакт-диска. Как мы видим на изображении выше, невозможно добавить или удалить промежуточный стек. Подобно этой аналогии, элементы, которые входят в стек вызовов, должны быть вставлены или удалены с одного и того же конца, который является вершиной стека. Это также известно как «последний вошел, первый вышел», что буквально означает, что самый последний вставленный элемент должен быть удален первым, прежде чем можно будет удалить любые другие элементы.

Я уверен, что многие из вас уже знакомы с концепцией push и pop. Если вы не знакомы с этим, вот простое объяснение: Push вставляет элемент в стек (массив), а pop удаляет последний элемент из стека (массива).

Элементы могут помещаться в стек только до тех пор, пока стек не будет исчерпан, что означает, что их можно помещать только до тех пор, пока в стеке не останется места. Если элементы помещаются больше, чем размер стека, происходит переполнение стека. При переполнении стека программа аварийно завершает работу и может либо зависнуть, либо закрыть браузер или программу. Вы могли столкнуться с чем-то подобным, создав функцию бесконечной рекурсии, которая никогда не заканчивается. Просто мы не хотим, чтобы это произошло.

Чтобы предотвратить переполнение стека, мы можем использовать несколько подходов, таких как использование динамического массива, запрет рекурсии и циклов, использование методов тестирования и измерения и многие другие. Но для целей и намерений этого поста мы не будем погружаться в конкретные примеры.

Так как же языки программирования, такие как JavaScript, используют стек вызовов для выполнения кода? В JavaScript память выделяется в стеке и куче. В частности, стек используется как высокопроизводительная непрерывная область памяти, используемая для выполнения функций с использованием логики, которую мы обсуждали. Давайте посмотрим на диаграмму ниже.

Вот что происходит на схеме:

  1. first() помещается в стек вызовов. console.log("Привет от первого!") выполняется немедленно.
  2. second() внутри first() помещается в стек вызовов. console.log("Привет от второго!") выполняется немедленно.
  3. Third() внутри second() помещается в стек вызовов. console.log("Привет от третьего!") выполняется немедленно.
  4. После последнего выполнения больше нет функции для выполнения. Итак, Third() выталкивается из стека вызовов.
  5. Затем из стека вызовов выталкивается метод second().
  6. Наконец, first() выталкивается из стека вызовов и оставляет пустой стек вызовов.

Как асинхронная функция и промисы работают со стеком вызовов?

Поскольку JavaScript является однопоточным языком программирования, это означает, что он имеет только один стек вызовов. Таким образом, он может выполнять одно действие за раз. Этот способ выполнения отлично работает с синхронными функциями, но все мы знаем, что есть и другие функции, которые не являются синхронными, например, асинхронные функции и промисы.

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

1. Синхронные функции

2. Обратные вызовы Promise или Microtask

3. Асинхронные обратные вызовы задач.

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

Вот что происходит при этом выполнении кода:

  1. Third() помещается в стек вызовов. console.log("Привет от третьего!") выполняется немедленно.
  2. setTimeout(first()) вызывается и сначала помещается в стек вызовов, затем перемещается в очередь задач и ждет.
  3. second() помещается в стек вызовов. console.log("Привет от второго!") выполняется немедленно.
  4. как только все выполнение завершено, second() и Third() выталкиваются из стека вызовов.
  5. Как только стек опустеет, first() возвращается в стек вызовов, и console.log("Привет от первого!") выполняется немедленно.
  6. Наконец, first() выталкивается из стека вызовов.

Как вы видели в приведенном выше примере, стек вызовов по-прежнему используется для асинхронной функции, но с несколькими дополнительными шагами. Концепция структуры данных стека остается прежней.

Краткое содержание

Стек — это структура данных, которая содержит функции, которые в конечном итоге будут выполняться одна за другой в соответствии с логикой «последним пришел — первым вышел». Многие языки используют push и pop для вставки и удаления элементов из стека вызовов. Когда элементов помещается больше, чем может обработать стек, происходит переполнение стека.

Я надеюсь, что вы нашли этот пост полезным, и я хочу верить, что вы уйдете с лучшим пониманием стека вызовов.

Использованная литература:

https://www.youtube.com/watch?v=sFVxsglODoo

https://www.freecodecamp.org/news/understanding-the-javascript-call-stack-861e41ae61d4/

https://medium.com/swlh/in-depth-introduction-to-call-stack-in-javascript-a07b8513bcc3