en.javascript.info/1-js/2-first-steps/19-recursion/article.md
Ilya Kantor 87bf53d076 update
2014-11-16 01:40:20 +03:00

273 lines
No EOL
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Рекурсия, стек
В коде функции могут вызывать другие функции для выполнения подзадач. Частный случай подвызова -- когда функция вызывает сама себя. Это называется *рекурсией*.
В этой главе мы рассмотрим, как рекурсия устроена изнутри, и как её можно использовать.
[cut]
## Реализация pow(x, n) через рекурсию
Чтобы возвести `x` в натуральную степень `n` -- можно умножить его на себя `n` раз в цикле:
```js
function pow(x, n) {
var result = x;
for(var i=1; i<n; i++) {
result *= x;
}
return result;
}
```
А можно поступить проще.
Ведь <code>x<sup>n</sup> = x * x<sup>n-1</sup></code>, т.е. можно вынести один `x` из-под степени. Иначе говоря, значение функции `pow(x,n)` получается из `pow(x, n-1)` умножением на `x`.
Этот процесс можно продолжить. Например, вычислим `pow(2, 4)`:
```js
pow(2, 4) = 2 * pow(2, 3) = 2 * 2 * pow(2, 2) = 2 * 2 * 2 * pow(2, 1) = 2 * 2 * 2 * 2;
```
Процесс перехода от `n` к `n-1` останавливается на `n==1`, так как очевидно, что `pow(x,1) == x`.
Код для такого вычисления:
```js
//+ run
function pow(x, n) {
if (n != 1) { // пока n!=1 сводить вычисление pow(..n) к pow(..n-1)
return x * pow(x, n-1);
} else {
return x;
}
}
alert( pow(2, 3) ); // 8
```
Говорят, что "функция `pow` *рекурсивно вызывает сама себя*" при `n != 1`.
Значение, на котором рекурсия заканчивается называют *базисом рекурсии*. В примере выше базисом является `1`.
Общее количество вложенных вызовов называют *глубиной рекурсии*. В случае со степенью, всего будет `n` вызовов. Максимальная глубина рекурсии ограничена и составляет около `10000`, но это число зависит от конкретного интерпретатора JavaScript и может быть в 10 раз меньше.
**Рекурсию используют, когда вычисление функции можно свести к её более простому вызову, а его -- еще к более простому, и так далее, пока значение не станет очевидно.**
## Контекст выполнения, стек
Теперь мы посмотрим, как работают рекурсивные вызовы. Для этого мы рассмотрим, как вообще работают функции, что происходит при вызове.
**У каждого вызова функции есть свой "контекст выполнения" (execution context).**
Контекст выполнения -- это служебная информация, которая соответствует текущему запуску функции. Она включает в себя локальные переменные функции.
Например, для вызова:
```js
//+ run
function pow(x, n) {
if (n != 1) { // пока n!=1 сводить вычисление pow(..n) к pow(..n-1)
return x * pow(x, n-1);
} else {
return x;
}
}
*!*
alert( pow(2, 3) ); // (*)
*/!*
```
При запуске функции `pow` в строке `(*)` будет создан контекст выполнения, который будет хранить переменные `x = 2, n = 3`. Мы схематично обозначим его так:
<ul class="function-execution-context">
<li>Контекст: { x: 2, n: 3 }</li>
</ul>
Далее функция `pow` начинает выполняться. Вычисляется выражение `n != 1` -- оно равно `true`, ведь в текущем контексте `n=3`. Поэтому задействуется первая ветвь `if` :
```js
if (n != 1) { // пока n!=1 сводить вычисление pow(..n) к pow(..n-1)
*!*
return x * pow(x, n-1);
*/!*
} else {
return x;
}
```
Чтобы вычислить выражение `x * pow(x, n-1)`, требуется произвести запуск `pow` с новыми аргументами.
**При любом вложенном вызове JavaScript запоминает место, где он остановился в текущей функции в специальной внутренней структуре данных -- "стеке контекстов".**
Это как если бы мы куда-то ехали, но очень захотелось поесть. Можно остановиться у кафе, оставить машину, отойти, а потом, через некоторое время, вернуться к ней и продолжить дорогу.
Так и здесь -- мы запомним, где остановились в этой функции, пойдём выполним вложенный вызов, затем вернёмся и продолжим дорогу.
**После того, как текущий контекст выполнения сохранён в стеке контекстов, JavaScript приступает к выполнению вложенного вызова.**
В данном случае вызывается та же `pow`, однако, это абсолютно неважно. Для любых функций процесс одинаков.
**Создаётся новый контекст выполнения, и управление переходит в подвызов, а когда он завершён -- старый контекст достаётся из стека и выполнение внешней функции возобновляется.**
## Разбор примера
Разберём происходящее более подробно, начиная с вызова `(*)`:
```js
//+ run
function pow(x, n) {
if (n != 1) { // пока n!=1 сводить вычисление pow(..n) к pow(..n-1)
return x * pow(x, n-1);
} else {
return x;
}
}
*!*
alert( pow(2, 3) ); // (*)
*/!*
```
<dl>
<dt>`pow(2, 3)`</dt>
<dd>Запускается функция `pow`, с аргументами `x=2`, `n=3`. Эти переменные хранятся в контексте выполнения, схематично изображённом ниже:
<ul class="function-execution-context">
<li>Контекст: { x: 2, n: 3 }</li>
</ul>
Выполнение в этом контексте продолжается, пока не встретит вложенный вызов в строке 3.
</dd>
<dt>`pow(2, 2)`</dt>
<dd>В строке `3` происходит вложенный вызов `pow` с аргументами `x=2`, `n=2`. Для этой функции создаётся новый текущий контекст (выделен красным), а предыдущий сохраняется в "стеке":
<ul class="function-execution-context">
<li>Контекст: { x: 2, n: 3 }</li>
<li>Контекст: { x: 2, n: 2 }</li>
</ul>
</dd>
<dt>`pow(2, 1)`</dt>
<dd>Опять вложенный вызов в строке `3`, на этот раз -- с аргументами `x=2`, `n=1`. Создаётся новый текущий контекст, предыдущий добавляется в стек:
<ul class="function-execution-context">
<li>Контекст: { x: 2, n: 3 }</li>
<li>Контекст: { x: 2, n: 2 }</li>
<li>Контекст: { x: 2, n: 1 }</li>
</ul>
</dd>
<dt>Выход из `pow(2, 1)`.</dt>
<dd>При выполнении `pow(2, 1)`, в отличие от предыдущих запусков, выражение `n != 1` будет равно `false`, поэтому сработает вторая ветка `if..else`:
```js
function pow(x, n) {
if (n != 1) {
return x * pow(x, n-1);
} else {
*!*
return x; // первая степень числа равна самому числу
*/!*
}
}
```
Здесь вложенных вызовов нет, так что функция заканчивает свою работу, возвращая `2`. Текущий контекст больше не нужен и удаляется из памяти, из стека восстанавливается предыдущий:
<ul class="function-execution-context">
<li>Контекст: { x: 2, n: 3 }</li>
<li>Контекст: { x: 2, n: 2 }</li>
</ul>
Возобновляется обработка внешнего вызова `pow(2, 2)`.
</dd>
<dt>Выход из `pow(2, 2)`.</dt>
<dd>...И теперь уже `pow(2, 2)` может закончить свою работу, вернув `4`. Восстанавливается контекст предыдущего вызова:
<ul class="function-execution-context">
<li>Контекст: { x: 2, n: 3 }</li>
</ul>
Возобновляется обработка внешнего вызова `pow(2, 3)`.
</dd>
<dt>Выход из `pow(2, 3)`.</dt>
<dd>Самый внешний вызов заканчивает свою работу, его результат: `pow(2, 3) = 8`.</dd>
</dl>
Глубина рекурсии в данном случае составила: **3**.
Как видно из иллюстраций выше, глубина рекурсии равна максимальному числу контекстов, одновременно хранимых в стеке.
[smart]
В самом конце, как и в самом начале, выполнение попадает во внешний код, который находится вне любых функций.
Контекст, который соответствует самому внешнему коду, называют *"глобальный контекст"*. Естественно, он является начальной и конечной точкой любых вложенных подвызовов.
[/smart]
Обратим внимание на требования к памяти. Рекурсия приводит к хранению всех данных для неоконченных внешних вызовов в стеке, в данном случае это приводит к тому, что возведение в степень `n` хранит в памяти `n` различных контекстов.
Реализация степени через цикл гораздо более экономна:
```js
function pow(x, n) {
var result = x;
for(var i=1; i<n; i++) {
result *= x;
}
return result;
}
```
У такой функции `pow` будет один контекст, в котором будут последовательно меняться значения `i` и `result`.
**Любая рекурсия может быть переделана в цикл. Как правило, вариант с циклом будет эффективнее.**
...Но зачем тогда нужна рекурсия? Да просто затем, что рекурсивный код может быть гораздо проще и понятнее!
Переделка в цикл может быть нетривиальной, особенно когда в функции, в зависимости от условий, используются разные рекурсивные подвызовы.
В программировании мы в первую очередь стремимся сделать сложное простым, а повышенная производительность нужна... Лишь там, где она действительно нужна. Поэтому красивое рекурсивное решение во многих случаях лучше.
Недостатки и преимущества рекурсии:
[compare]
-Требования к памяти.
-Ограничена максимальная глубина стека.
+Краткость и простота кода.
[/compare]
## Итого
Рекурсия -- это когда функция вызывает сама себя, с другими аргументами.
Существуют много областей применения рекурсивных вызовов. Здесь мы посмотрели на один из них -- решение задачи путём сведения её к более простой (с меньшими аргументами), но также рекурсия используется для работы с "естественно рекурсивными" структурами данных, такими как HTML-документы, для "глубокого" копирования сложных объектов.
Есть и другие применения, с которыми мы встретимся по мере изучения JavaScript.
Здесь мы постарались рассмотреть происходящее достаточно подробно, однако, если пожелаете, допустимо временно забежать вперёд и открыть главу [](/debugging-chrome), с тем чтобы при помощи отладчика построчно пробежаться по коду и посмотреть стек.
[head]
<style>
.function-execution-context {
margin: 0;
padding: 0;
overflow: auto;
}
.function-execution-context li {
float: left;
clear: both;
border: 1px solid black;
font-family: "PT Mono", monospace;
padding: 3px 5px;
}
.function-execution-context li:last-child {
color: red;
}
</style>
[/head]