Структуры данных стека работают за кулисами многих языков программирования, включая Javascript, и, что важно, когда речь идет о стеке вызовов.

Хотя знание того, как Javascript реализует структуру данных стека во время выполнения, не является обязательным условием для написания вашего первого кода Javascript, понимание того, что такое структура данных стека и как она используется в Javascript, необходимо для понимания того, как работают ваши функции. и как отлаживать некоторые проблемы, с которыми вы можете столкнуться. Понимание стека вызовов также является необходимым условием для понимания модели циклов событий Javascript и возможности избежать сложных проблем, когда речь идет об асинхронных событиях в вашем коде (например, загрузка изображения из внешнего источника или запуск функции). на основе прослушивателя событий).

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

Что такое стековая структура данных?

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

Простой способ представить эту концепцию в Javascript — использовать методы push() и pop(), доступные для массивов.

const array = [1, 2, 3]

array.push(4)
//array = [ 1, 2, 3, 4 ]

array.pop()
//array = [ 1, 2, 3 ]

Как видите, push() всегда будет добавлять элемент в конец массива, а pop() всегда будет удалять элемент из конца.

Структура данных стека и стек вызовов

Всякий раз, когда часть кода Javascript запускается с помощью NodeJS или вашего браузера, механизм Javascript автоматически создает так называемый «стек вызовов», чтобы организовать выполнение вашего кода. Во время выполнения движок будет складывать задачи для выполнения снизу вверх, а затем выполнять стек задач сверху вниз, пока стек не будет очищен. Такая организация важна, особенно когда функции вызывают друг друга в вашем коде, потому что она гарантирует, что события будут происходить в правильном порядке. Javascript имеет однопоточную среду выполнения, что просто означает, что существует только один стек вызовов, а задачи из стека вызовов выполняются в том порядке, в котором они добавляются последними, первыми. вне стиля.

Простой пример (в NodeJS)

В качестве простого примера этот код определяет и запускает функцию addTwo(), которая, в свою очередь, вызывает функцию addOne().

function addOne(num) {
  return num + 1;
}

function addTwo(num) {
  return addOne(num) + 1;
}

addTwo(1);

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

Первой вызывается функция addTwo() , поэтому она добавляется в стек вызовов следующей. addTwo() затем вызывает addOne() , поэтому add one также добавляется в стек вызовов.

После завершения addOne() он удаляется из стека вызовов, и движок запускает addTwo() с возвращаемым значением из addOne(). Наконец, поскольку весь код в файле выполнен, стек вызовов очищается и выполнение кода завершается.

Пример с рекурсией (в браузере)

Хотя приведенный выше пример был запущен с NodeJS, та же реализация стека вызовов используется для запуска кода в браузере с некоторыми интересными последствиями для рендеринга веб-страниц.

Для начала у меня есть простая веб-страница с одним HTML-элементом h1, содержащим текст «0».

 ///recursive function 
  const addOneToNum = function(num) {
    if (num < 5) {              //if number is less than 5
      num = num + 1;            //add one to the number
      h1.innerHTML = num;       //set the text on the page to the value of the number
      return addOneToNum(num);  //call itself to run another iteration
    }
    return;                     //if the number is equal to or greater than 5, return                        
  }
  addOneToNum(h1.innerText)     //call the function (on click in this case)
})

В приведенном выше коде я определил рекурсивную функцию, которая принимает число (значение со страницы). Когда функция вызывается, она добавляет 1 к предоставленному числу, устанавливает число на странице равным значению числа, а затем вызывает себя с новым значением числа, пока число не станет равным или больше 5. Эта функция первоначально вызывается со значением текста HTML, когда кто-то щелкает на странице. Несмотря на то, что для этой работы требуется дополнительный код, в данном примере это не важно.

Надеюсь, вы видите, что по мере продвижения по функции все вызовы addOneToNum() добавляются в стек вызовов один за другим. Наконец, как только число достигает 5, функции возвращаются и удаляются из вершины стека вызовов до тех пор, пока не останется задач.

Примечание об асинхронном коде

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

Хотя я все еще использую тот же код, текст на странице сразу переходит от 0 к 5, без отображения промежуточных чисел!

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

Однако если я добавлю в код функцию таймера, мы увидим, что она работает так, как хотелось бы:

const addOneToNum = function (num) {
    setTimeout(() => { //set timeout for running this code

      if (num < 5) {   //if number is less than 5                   
        num++;         //add one to the number
        h1.innerHTML = num; //set the text on the page to the value of the number
        return addOneToNum(num); //call itself to run another iteration
      }
      return;          //if the number is equal to or greater than 5, return

    }, 500);          //wait 500 milliseconds to execute
  };

  addOneToNum(startingValue); //call the function (on click in this case)
});

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

Переполнение стека

Вы можете задаться вопросом, как сайт «Переполнение стека» получил свое название. Ответ связан с тем, как реализован стек вызовов, и представляет собой сообщение об ошибке, которое вы почти наверняка увидите в какой-то момент своего пути кодирования.

Что, если я изменю код из последнего раздела на этот:

const addOneToNum = function(num) {
      num++                     //add one to the number
      h1.innerHTML = num;       //set the text on the page to the value of the number
      return addOneToNum(num)   //call itself to run another iteration
  }


  addOneToNum(startingValue)    //call the function (on click in this case)

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

Визуально, когда я использую этот код, это выглядит так:

И в консоли браузера получаю такое сообщение:

Откуда 10958!? В данном случае речь идет о количестве задач, которые были зарегистрированы в стеке вызовов до того, как браузер выдал ошибку. Как правило, в браузере или другой рабочей среде для вашего кода существует максимальный размер стека вызовов. Это количество задач, которые могут быть поставлены в очередь в стеке вызовов, прежде чем возникнет ошибка (в данном случае 10958 + 2, которые не были захвачены в отображаемом числе). В то время как Chrome сообщает нам «превышен максимальный размер стека вызовов», другие работающие среды могут отображать сообщение «переполнение стека», откуда и происходит этот термин. Именно из-за ошибок такого типа важно включать в код условие остановки при запуске функций, которые могут генерировать бесконечные циклы.

Заключение

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

Ресурсы

Что такое стек вызовов в Javascript?

Стек вызовов

Цикл событий

Что, черт возьми, за цикл событий? (видео)