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