Иногда для решении задачи приходится использовать Рекурсию, в которой есть свои плюсы и минусы. Я столкнулся с проблемой переполнения стека.
Максимальная глубина рекурсии ограничена движком JavaScript. Точно можно рассчитывать на 10000 вложенных вызовов, некоторые интерпретаторы допускают и больше, но для большинства из них 100000 вызовов – за пределами возможностей. Существуют автоматические оптимизации, помогающие избежать переполнения стека вызовов («оптимизация хвостовой рекурсии»), но они ещё не поддерживаются везде и работают только для простых случаев.
Пример рекурсивной функции:
function sum(n) { return n === 0 ? 0 : n + sum(n - 1) } sum(5) // 1 + 2 + 3 + 4 + 5 = 15 sum(100000) // Error: Maximum call stack size exceeded.
Хвостовая рекурсия позволяет оптимизировать вызовы компилятором и уже есть в стандарте ES6, но поддержка браузерами оставляет желать лучшего.
Пример хвостовой рекурсивной функции:
function sum(number, s = 0){ return number === 0 ? s : sum(number - 1, s + number) }
Но, без поддержки браузерами мы столкнемся с той же проблемой — переполнения стека. Можем попробовать использовать вместе с Trampolining.
Напишем декоратор, который будет принимать измененную рекурсивную функцию(следующий шаг) и исполнять ее в цикле, без увеличения глубины вызовов.
function trampoline(fn) { return function(...args) { let result = fn.apply(fn.context, args) while (typeof result === 'function') { result = result() } return result } }
Теперь наша рекурсивная функция должна возвращать функцию, которая будет сразу исполняться декоратором. Таким способом, мы условно сделаем массив функций и исполним их по очереди. Но поскольку мы каждый раз возвращаем новую анонимную функцию, этот код будет работать несколько медленнее.
function sum(number, s = 0) { return number === 0 ? s : () => sum(number - 1, s + number) }
Используем:
const trampolineSum = trampoline(sum) trampolineSum(100000) // 5000050000
Это моя первая статья. Я старался не пересказывать понятия и указывал ссылку на источники. Тема не уникальная и давно существует на англоязычных сайтах, к сожалению на русском не нашел.
Спасибо, за внимание. Хорошего дня :)
Update
Производительность:
#1 Рекурсия
function sum(n) { return n === 0 ? 0 : n + sum(n - 1) } console.time("run") sum(1000) console.timeEnd("run")
[Debug] run: 0.281ms
[Debug] run: 0.305ms
[Debug] run: 0.315ms
[Debug] run: 0.319ms
[Debug] run: 0.231ms
[Debug] run: 0.255ms
[Debug] run: 0.334ms
[Debug] run: 0.370ms
[Debug] run: 0.274ms
[Debug] run: 0.260ms
run: 0.101806640625ms
run: 0.099853515625ms
run: 0.10205078125ms
run: 0.10302734375ms
run: 0.099853515625ms
run: 0.106201171875ms
run: 0.103759765625ms
run: 0.105224609375ms
run: 0.262939453125ms
run: 0.136962890625ms
run: 0.10107421875ms
run: 0.10302734375ms
#2 Итерация
function sum(n) { let sum = 0 for (let i = 1; i <= n; i++) { sum += i } return sum } console.time("run") sum(1000) console.timeEnd("run")
[Debug] run: 0.552ms
[Debug] run: 0.502ms
[Debug] run: 0.527ms
[Debug] run: 0.434ms
[Debug] run: 0.462ms
[Debug] run: 0.511ms
[Debug] run: 0.528ms
[Debug] run: 0.549ms
[Debug] run: 0.475ms
[Debug] run: 0.530ms
[Debug] run: 0.494ms
run: 0.751953125ms
run: 0.4580078125ms
run: 0.678955078125ms
run: 0.424072265625ms
run: 0.505859375ms
run: 0.563720703125ms
run: 0.404052734375ms
run: 0.411865234375ms
run: 0.634033203125ms
run: 0.4169921875ms
run: 0.390869140625ms
run: 0.464111328125ms
#3 Хвостовая рекурсия и «батут»
function trampoline(fn) { return function(...args) { let result = fn.apply(fn.context, args) while (typeof result === 'function') { result = result() } return result } } function sum(number, s = 0) { return number === 0 ? s : () => sum(number - 1, s + number) } const trampolineSum = trampoline(sum) console.time("run") trampolineSum(1000) console.timeEnd("run")
[Debug] run: 0.792ms
[Debug] run: 0.882ms
[Debug] run: 0.826ms
[Debug] run: 0.968ms
[Debug] run: 0.818ms
[Debug] run: 1.681ms
[Debug] run: 0.777ms
[Debug] run: 1.109ms
[Debug] run: 0.832ms
[Debug] run: 0.826ms
run: 0.989990234375ms
run: 0.567138671875ms
run: 0.56005859375ms
run: 1.0087890625ms
run: 0.5400390625ms
run: 0.578125ms
run: 0.541015625ms
run: 0.557861328125ms
run: 1.97607421875ms
run: 0.570068359375ms
run: 0.593017578125ms
run: 0.530029296875ms
run: 0.89794921875ms
run: 0.590087890625ms
#4 Хвостовая рекурсия и «батут» (большое число)
function trampoline(fn) { return function(...args) { let result = fn.apply(fn.context, args) while (typeof result === 'function') { result = result() } return result } } function sum(number, s = 0) { return number === 0 ? s : () => sum(number - 1, s + number) } const trampolineSum = trampoline(sum) console.time("run") trampolineSum(100000) console.timeEnd("run")
[Debug] run: 24.564ms
[Debug] run: 25.313ms
[Debug] run: 23.262ms
[Debug] run: 24.848ms
[Debug] run: 23.909ms
[Debug] run: 24.248ms
[Debug] run: 32.416ms
[Debug] run: 24.090ms
[Debug] run: 23.986ms
run: 33.955078125ms
run: 40.907958984375ms
run: 37.693115234375ms
run: 28.929931640625ms
run: 30.7548828125ms
run: 29.720947265625ms
run: 40.8310546875ms
run: 31.5830078125ms
run: 30.712890625ms
run: 30.162841796875ms
run: 31.56103515625ms
По результатам мы видим, что наш декоратор хоть и позволяет избежать ошибки переполнения стека, но работает он медленнее чем рекурсивный и итеративный вариант. Так что данный способ стоит использовать только если вы не можете заменить рекурсию на итерацию или боитесь переполнения стека при выполнении вашей рекурсивной функции.

