Объясните пример с рекурсией

Добрый день.
Объясните пожалуйста в этом примере получается 8

function power (base, exponent) {
  if(exponent == 0) {
    return 1;
  }
  else {
    return base * power(base, exponent -1)
  }
}

console.log(power(2, 3));

не пойму на что умножается base, если дочерняя функция не возвращает никакой результат, а только вызов родительской функции…

Ключ в том что вызов функции и есть результат, используя терминологию из вопроса.

Вызов функции power(base, exponent -1) вычисляется в некоторое значение которое потом используется в base * power(base, exponent -1). И это вычисляемое значение в свою очередь вычисляется через вызов этой же power. И так по цепочке вычисление power(2, x) будет требовать вычисления power(2, x - 1). До тех пор пока последний вызов в цепочке power(2, 0) не вычислится в 1 благодаря этим строкам:

if(exponent == 0) {
  return 1;
}

Это рекурсия. Она обычно не легко ложится в голову потому что принципиально отличается от всего известного опыта. Нужно посидеть-подумать-помучаться. Понимание прийдет со временем.


И чтобы дваждны не вставать. Бывает полезно почитать разные объяснения одного и того же чтобы понять суть. Вот еще одно объяснение-ответ от ИИ на этот вопрос:

Давайте разберем этот пример шаг за шагом, чтобы понять, как работает рекурсия в данной функции power(base, exponent), которая вычисляет base в степени exponent.

Вызов функции power(2, 3) означает, что мы хотим вычислить 2 в степени 3, то есть 2 * 2 * 2.

Шаги выполнения функции power(2, 3):

  1. Первый вызов: power(2, 3)

    • exponent не равен 0, поэтому функция переходит к else блоку и возвращает 2 * power(2, 2).
  2. Второй вызов: power(2, 2)

    • Это вызов происходит внутри первого вызова. Теперь exponent равен 2, что снова не равно 0. Следовательно, функция возвращает 2 * power(2, 1).
  3. Третий вызов: power(2, 1)

    • Аналогично, exponent теперь равен 1, что приводит к возврату 2 * power(2, 0).
  4. Четвертый вызов: power(2, 0)

    • На этот раз exponent равен 0, что соответствует условию базы рекурсии. Функция возвращает 1 и не производит дальнейших вызовов.

Возврат значений:

  • Четвертый вызов возвращает 1.
  • Третий вызов получает это значение и возвращает 2 * 1 = 2.
  • Второй вызов получает значение от третьего вызова и возвращает 2 * 2 = 4.
  • Первый вызов получает значение от второго вызова и возвращает 2 * 4 = 8.

Таким образом, каждый вызов функции действительно возвращает результат. Рекурсивные вызовы функции power(base, exponent - 1) уменьшают значение exponent на каждом шаге, пока не достигнут базового случая (exponent == 0), который прерывает рекурсию и начинает обратный процесс возврата значений, которые последовательно умножаются, в итоге давая конечный результат вычисления степени.

Надеюсь, это объяснение помогло вам понять, как работает данная рекурсивная функция.

спасибо, стало понятней до вот этой фразы: “Четвертый вызов возвращает 1.” а дальше, почему выполнение переходит к третьему вызову? ведь return 1; должен завершить выполнение?

Вычисление не останавливается. Третий вызов требует значения с четвертого для того чтобы вычислить себя. А второму вызову нужно значение с третьего. Интерпретатор как бы “погружается” вглубь вызовов, дойдя до дна (нерекурсивный вызов) и потом “всплывает” (это тот самый момент где судя по вопросу казалось бы что вычисление должно остановиться) до самого первого вызова с которой начались рекурсивные вызовы. Ниже - детальнее про это же самое.

power(2, 3)
	2 * power(2, 2)
		2 * power(2, 1)
			2 * power(2, 0)
				1

Выше - упрощенная модель той же рекурсии. Для вычисления power(2,3) нужно подставить (считай вычислить) значение power(2,2) в 2 * power(2, 2). А для вычисления (power 2, 2) нужно вычилить power(2, 1) чтобы подставить в 2 * power(2,1). итд.

Интерпретатор как-бы идет по этой лестнице вниз, где каждый шаг вниз описыват как вычисляется значения сверху. Потом доходит до “дна” где последнее значение не требует рекурсивного вызова для вычисления себя а вычисляется простым return 1;, и теперь он может подниматься ниже, вычисляя все значения выше.


А вот ответ ИИ на тот же вопрос в том же контексте:

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

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

В контексте вашего примера:

  • Четвертый вызов power(2, 0) возвращает 1. Этот вызов завершается и удаляется из стека вызовов. Управление возвращается к третьему вызову power(2, 1), который ждал результата четвертого вызова.
  • Третий вызов теперь может завершиться, потому что он получил значение, которое нужно было умножить на base (в данном случае 2). Он возвращает 2 * 1 = 2 и удаляется из стека вызовов. Управление возвращается к второму вызову power(2, 2).
  • Процесс продолжается аналогично, пока не будут завершены все вызовы функции и стек вызовов не опустеет.

Таким образом, return 1; завершает только текущий вызов функции в стеке вызовов, а не весь процесс или все рекурсивные вызовы функции. Каждый вызов функции завершается по очереди, возвращаясь к предыдущему вызову, пока не будет достигнут первоначальный вызов, который инициировал рекурсию. Это и есть механизм, благодаря которому рекурсия работает и возвращает конечный результат.

ух… но почему js ждет результата другого вызова? почему например не выкинул ошибку? ведь в любых других случаях при невозможности совершения каких-либо операций (у нас это умножение числа на функцию) получается либо какой-нибудь undefined, либо просто сообщение об ошибке возникает… мне кажется я здесь какой-то момент теоретический упустил, который в принципе касается работы функций в js…

Потому что каждая ступенька из описанного рекурсивного вызова вычисляема. Ошибка выкинится когда следущий шаг вычисления будет невозможен по той или иной причине. Например, ошибка появится когда ни один вызов функции не вычисляется из-за того что они все вызывают рекурсивно себя. Тогда у JS-а закончится возможность запоминания того какая функция ждлет результат вычисления какой функции (эта система запоминания называется call stack или стек вызова).

Пока бер рекурсии посмотрим на выражение 10 + getNum(). Чтобы его вычислить js находит функцию getNum и вызывает ее, выполняет все выражения и нее до самого return (явного или неявного), и, получив выражение подставляет его в выражение 10 + getNum().

А что если getNum внутри себя вызывает другую функцию, допустим getType? Тогда интерпретатор, вычисляя значение getNum, дойдет до строки вызова getType, и начинает вычислять ее значение проходя по всем выражениям внутри getType. A это время функция getNum как бы ждет вычисления getType, потому что результат вычисления нужен getNum для возврата значения вычисления себя. А что еслиgetTypeвызывает еще другую функцию допустимgetEnv? Тогда история повторяется, getTypeбудет ждать результата вызоваgetEnv. В это время getNumпродолжает "ждать"getType`.

Именно это ожидание, зависимость от результатов вложенных вызовов и визуально описано в лесенке которую я давал в предыдущем ответе.

Возвращаемся к рекурсии.

power(2, 1)
	2 * power(2, 0)
		1

js пытается вычислить power(2, 1). Выполняя тело функции power, ему для этого нужно вычислить 2 * power(2, 0), где на место второго множителя подставляется результат вызова power(2, 0). Результат вызова power(2,0) вычисляется в 1, подставляется в 2 * power(2,0) и получает результат вызова power(2, 1).


Ну и ИИ:

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

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

В контексте вашего примера, когда выполняется строка return base * power(base, exponent -1), JavaScript не пытается немедленно умножить base на результат функции power. Вместо этого, он сначала выполняет вызов power(base, exponent -1), ожидая его завершения. Каждый вызов power продолжает выполняться, пока не достигнет базового случая, который возвращает числовое значение (в данном случае 1), а не вызов функции. Только после того, как рекурсивный вызов возвращает число, JavaScript производит умножение и возвращает результат в предыдущий вызов функции.

Это означает, что в любой момент времени JavaScript точно знает, что делать с каждым выражением и операцией. Он не пытается умножить число на функцию; он умножает число на результат вызова функции, который является числом. Это ключевое различие, которое позволяет избежать ошибок, связанных с попыткой выполнить недопустимые операции, такие как умножение числа на функцию.

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

Огромнейшая благодарность!!!

Если все понятно, я могу пометить первый ответ как ответ на вопрос? Или пометь пожалуйста тот ответ который помог понять суть происходящего. Таким образом те кто будет нас читать позже смогут эффективнее разбираться самостоятельно.

конечно, пометьте, как вам удобно))) а суть происходящего из всех ответов я понял, поэтому как вам удобнее…

1 лайк