JavaScript 📜
February 2

Ограничения CallStack в JavaScript

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

const sumNum = (num) => {
    if (num === 1) {
        return 1;
    }
    return num + sumNum(num - 1);
}

В моём случае функция стабильно работает для sumNum(9648), а при попытке вызвать sumNum(9649) происходит ошибка "Maximum call stack size exceeded". Я задался вопросом, а как в других браузерах?

Стек вызовов и глубина рекурсии

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

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

Отсутствует какой-либо стандарт, регламентирующий глубину допустимых рекурсивных вызовов. За это отвечает сам движок. Я провёл небольшой эксперимент:

  • V8 (Chrome, Yandex и другие на его основе)
    Движок V8, который используется в Google Chrome и его производных, реализует стек вызовов таким образом, что ограничение находится в районе 9640 вызовов. Самый "глубокий" вызов оказался в Яндекс Браузере, а хром отстал всего на пару вызовов.
  • SpiderMonkey (Firefox)
    Firefox использует движок SpiderMonkey, и имеет куда более скромные показатели глубины рекурсивных вызовов - всего 1844 вызовов.

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