Перевод: Привет, Хабр! Представляю вашему вниманию перевод статьи "A Quick Intro to Recursion in Javascript" Yazeed Bzadough.

Примечание. Рекурсия не единожды обсуждалась на хабре, но данная статья даёт базовое понимание рекурсии. Это будет полезно начинающим разработчикам. Также, данная статья является моим первым переводом, поэтому прошу оставлять свои комментарии с конструктивной критикой по переводу и статье.

recursion_1


Функция продолжает вызывать себя пока не будет остановлена.


Начинающие разработчики часто плохо понимают рекурсию. Возможно, это вызвано тем, что в обучающих материалах часто используются алгоритмические примеры (числа Фибоначчи, связные списки).


В данной статье мы будем использовать всего один простой пример.


Основная идея


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


recursion_2


Рекурсивные функции позволяют выполнить одно и тоже действие несколько раз. Это как раз то, для чего используются циклы for и while. Иногда рекурсия является более элегантным решением задачи.


Функция обратного отсчёта


Давайте создадим функцию обратного отсчёта, которая будет вести отсчёт от заданного числа.
Использовать данную функцию мы будем следующим образом:


countDownFrom(5);
// 5
// 4
// 3
// 2
// 1

Алгоритм решения задачи можно описать так:


  1. Получаем параметр number. Это наша отправная точка.
  2. Идём от значения параметра number до 0, логируя результат.

Сначала рассмотрим решение с помощью цикла, а затем рекурсивное решение.


Императивный подход (циклы)


function countDownFrom(number) {
    for (let i = number; i > 0; i--) {
        console.log(i);
    }   
}

countDownFrom(5);
// 5
// 4
// 3
// 2
// 1

Это решение содержит оба шага алгоритма, описанного выше.


  1. Получаем параметр number. Это наша отправная точка.
  2. Идём от значения параметра number до 0, логгируя результат.

Рекурсивный подход


function countDownFrom(number) {
    if (number === 0) {
        return;
    }

    console.log(number);    
    countDownFrom(number - 1);
}

countDownFrom(5);
// 5
// 4
// 3
// 2
// 1

Это решение также содержит все шаги:


This one also passes.


  1. Получаем параметр number. Это наша отправная точка.
  2. Идём от значения параметра number до 0, логгируя результат.

Оба варианта приводят к одному результату, но разными путями.


Отладка императивного решения


Для большей наглядности добавим debugger в наше императивное решение и проследим за выполнением, используя Chrome Developer Tools.


function countDownFrom(number) {
    for (let i = number; i > 0; i--) {
        console.log(i);
        debugger;
    }   
}

recursion_3


Видите как дополнительная переменная i используется для отслеживания текущего значения number?


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


Отладка рекурсивного решения


function countDownFrom(number) {
    if (number === 0) {
        return;
    }

    console.log(number);

    debugger;

    countDownFrom(number - 1);
}

recursion_4


Рекурсивной версии не нужна дополнительная переменная для отслеживания прогресса выполнения. Обратили внимание на то, как растёт стек вызовов?


Это вызвано тем, что каждый вызов countDownFrom добавляется в стек со значением number - 1.


Так мы каждый раз передаём обновленное значение number. И нам не нужны дополнительные переменные!


Это основное отличие между двумя версиями.


Итеративное решение использует внутреннее состояние (дополнительные переменные для отсчёта и т. д.), а рекурсивное решение просто создает новый вызов с обновленным параметром.


Но как обе версии узнают когда необходимо остановиться?


Бесконечные циклы


Возможно, вы уже знаете о ужасных бесконечных циклах:


 THIS RUNS FOREVER, BE WARNED 
while (true) { console.log('WHY DID YOU RUN THIS?!' }

 THIS RUNS FOREVER, BE WARNED 
for (i = 0;;) { console.log('WHY DID YOU RUN THIS?!') }

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


Примечание переводчика: завершение работы будет связано с достижением максимального размера стека вызовов. Максимальный размер стека вызовов зависит от конкретного браузера и может сильно варьироваться. Перейдя по ссылке Вы можете оценить предел рекурсивных вызовов функции в вашем браузере и сравнить его с результатами других пользователей

Вы можете предотвратить это добавив условие выхода из цикла.


 This does not run forever
x = 0;
while (x < 3) { console.log(x); x++; }

 This does not run forever
for (x = 0; x < 3; x++) { console.log(x); }

В обоих случаях мы логируем значение x, инкрементируем x, и останавливаем выполнение когда значение x становится равно 3. Наша функция countDownFrom использует схожую логику:
Our countDownFrom function had similar logic.


// Stop at 0
for (let i = number; i > 0; i--)

Повторим. Циклам необходима дополнительная переменная, чтобы они могли остановиться.


Бесконечная рекурсия


Рекурсия также представляет опасность. Очень легко написать рекурсивную функцию, которая приведёт к завершению работы браузера.


THIS RUNS FOREVER, BE WARNED
function run() {
    console.log('running');
    run();
}

run();
// running
// running
// ...

recursion_5


Без условия остановки, функция run будет бесконечно вызывать сама себя. Вы можете исправить это с помощью условия(if):


 You can fix that with an if statement.

 This does not run forever

function run(x) {
    if (x === 3) return;

    console.log('running');
    run(x + 1);
}

run(0);
// running
// running
// running

// x is 3 now, we're done.

Базовое условие


Наша рекурсивная функция countDownFrom имеет одно базовое условие


if (number === 0) {
    return;
}

Здесь та же идея, что и в цикле. Всегда нужно помнить, что необходимо базовое условие.


recursion_6


Выводы


  • Рекурсией называется процесс когда функция вызывает сама себя пока не будет остановлена
  • Рекурсию можно использовать вместо циклов
  • Если не остановить рекурсию, она приведёт к "падению" вашей программы
  • Базовое условие останавливает рекурсию. Не забывайте о нём!
  • Циклы используют переменные состояния для отслеживания и отсчёта. В рекурсии используются только переданные параметры.
    recursion_7

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

Комментарии (8)


  1. lolmaus
    27.11.2019 19:56

    Ни слова про максимальный размер стэка.


    К примеру, countDownFrom(30000) в случае с циклом for отработает нормально в любом браузере, а в случае с рекурсией — упадет в Chrome и не упадет в Firefox. Почему? Загадка...


    Про хвостовую рекурсию тоже ни слова. :/


    PS Плюсик поставил.


    1. Pecheneg2015 Автор
      27.11.2019 20:01

      Благодарю. Автор позиционировал статью как простейшее объяснение работы рекурсивных функций и не углублялся в детали.
      Но Вы правы — добавлю примечания в статью.


      1. chapuza
        28.11.2019 17:26
        +1

        В хвостовой рекурсии (в языках, которые поддерживают Tail Call Optimization) самое главное то, что ей не нужен стек. И, как следствие, во всяких там эрлангах — можно хвостовой рекурсией посчитать Fib(100000000).

        lolmaus есть гипотеза, что разработчики FF озаботились имплементацией TCO, без него 30000 фреймов на стеке уронит вообще все, что угодно.


        1. lolmaus
          28.11.2019 17:58

          FF падает на 50—60 тысячах фреймов.


          В JS поддержка хвостовой рекурсии есть пока только в Safari.


          https://kangax.github.io/compat-table/es6/


          1. chapuza
            28.11.2019 18:21

            Ух! Ну, значит, они фреймы bzip’ом жмут :))


  1. Akuma
    27.11.2019 20:14
    +2

    Чтобы понять рекурсию достаточно перейти по этой ссылке и прочитать комментарий: habr.com/ru/post/477796/#comment_20938906

    Если все еще не понятно, то повторить.


    1. karl93rus
      28.11.2019 08:18

      Чтобы понять рекурсию, нужно понять рекурсию…


      1. Badimagination
        28.11.2019 09:06

        Чтобы понять рекурсию, нужно сначала понять рекурсию
        Вот так оно должно выглядеть