en.javascript.info/1-js/2-first-steps/20-recursion/article.md
Ilya Kantor 6baabac3c5 up
2015-09-21 15:04:14 +02:00

243 lines
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.

# Рекурсия, стек
В коде функции могут вызывать другие функции для выполнения подзадач.
Частный случай подвызова -- когда функция вызывает сама себя. Это называется *рекурсией*.
Рекурсия используется для ситуаций, когда выполнение одной сложной задачи можно представить как некое действие в совокупности с решением той же задачи в более простом варианте.
Сейчас мы посмотрим примеры.
Рекурсия -- общая тема программирования, не относящаяся напрямую к JavaScript. Если вы разрабатывали на других языках или изучали программирование раньше в ВУЗе, то наверняка уже знаете, что это такое.
Эта глава предназначена для читателей, которые пока с этой темой незнакомы и хотят лучше разобраться в том, как работают функции.
[cut]
## Степень pow(x, n) через рекурсию
В качестве первого примера использования рекурсивных вызовов -- рассмотрим задачу возведения числа `x` в натуральную степень `n`.
Её можно представить как совокупность более простого действия и более простой задачи того же типа вот так:
```js
pow(x, n) = x * pow(x, n - 1)
```
То есть, <code>x<sup>n</sup> = x * x<sup>n-1</sup></code>.
Например, вычислим `pow(2, 4)`, последовательно переходя к более простой задаче:
<ol>
<li>`pow(2, 4) = 2 * pow(2, 3)`</li>
<li>`pow(2, 3) = 2 * pow(2, 2)`</li>
<li>`pow(2, 2) = 2 * pow(2, 1)`</li>
<li>`pow(2, 1) = 2`</li>
</ol>
На шаге 1 нам нужно вычислить `pow(2,3)`, поэтому мы делаем шаг 2, дальше нам нужно `pow(2,2)`, мы делаем шаг 3, затем шаг 4, и на нём уже можно остановиться, ведь очевидно, что результат возведения числа в степень 1 -- равен самому числу.
Далее, имея результат на шаге 4, он подставляется обратно в шаг 3, затем имеем `pow(2,2)` -- подставляем в шаг 2 и на шаге 1 уже получаем результат.
Этот алгоритм на JavaScript:
```js
//+ run
function pow(x, n) {
if (n != 1) { // пока n != 1, сводить вычисление pow(x,n) к pow(x,n-1)
return x * pow(x, n - 1);
} else {
return x;
}
}
alert( pow(2, 3) ); // 8
```
Говорят, что "функция `pow` *рекурсивно вызывает сама себя*" до `n == 1`.
Значение, на котором рекурсия заканчивается называют *базисом рекурсии*. В примере выше базисом является `1`.
Общее количество вложенных вызовов называют *глубиной рекурсии*. В случае со степенью, всего будет `n` вызовов.
Максимальная глубина рекурсии в браузерах ограничена, точно можно рассчитывать на `10000` вложенных вызовов, но некоторые интерпретаторы допускают и больше.
Итак, рекурсию используют, когда вычисление функции можно свести к её более простому вызову, а его -- ещё к более простому, и так далее, пока значение не станет очевидно.
## Контекст выполнения, стек
Теперь мы посмотрим, как работают рекурсивные вызовы. Для этого мы рассмотрим, как вообще работают функции, что происходит при вызове.
**У каждого вызова функции есть свой "контекст выполнения" (execution context).**
Контекст выполнения -- это служебная информация, которая соответствует текущему запуску функции. Она включает в себя локальные переменные функции и конкретное место в коде, на котором находится интерпретатор.
Например, для вызова `pow(2, 3)` из примера выше будет создан контекст выполнения, который будет хранить переменные `x = 2, n = 3`. Мы схематично обозначим его так:
<ul class="function-execution-context">
<li>Контекст: { x: 2, n: 3, строка 1 }</li>
</ul>
Далее функция `pow` начинает выполняться. Вычисляется выражение `n != 1` -- оно равно `true`, ведь в текущем контексте `n=3`. Поэтому задействуется первая ветвь `if` :
```js
function pow(x, n) {
if (n != 1) { // пока n != 1 сводить вычисление pow(x,n) к pow(x,n-1)
*!*
return x * pow(x, n - 1);
*/!*
} else {
return x;
}
}
```
Чтобы вычислить выражение `x * pow(x, n-1)`, требуется произвести запуск `pow` с новыми аргументами.
**При любом вложенном вызове 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, строка 1 }</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, строка 3 }</li>
<li>Контекст: { x: 2, n: 2, строка 1 }</li>
</ul>
Обратим внимание, что контекст включает в себя не только переменные, но и место в коде, так что когда вложенный вызов завершится -- можно будет легко вернуться назад.
Слово "строка" здесь условно, на самом деле, конечно, запомнено более точное место в цепочке команд.
</dd>
<dt>`pow(2, 1)`</dt>
<dd>Опять вложенный вызов в строке `3`, на этот раз -- с аргументами `x=2`, `n=1`. Создаётся новый текущий контекст, предыдущий добавляется в стек:
<ul class="function-execution-context">
<li>Контекст: { x: 2, n: 3, строка 3 }</li>
<li>Контекст: { x: 2, n: 2, строка 3 }</li>
<li>Контекст: { x: 2, n: 1, строка 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, строка 3 }</li>
<li>Контекст: { x: 2, n: 2, строка 3 }</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, строка 3 }</li>
</ul>
Возобновляется обработка внешнего вызова `pow(2, 3)`.
</dd>
<dt>Выход из `pow(2, 3)`.</dt>
<dd>Самый внешний вызов заканчивает свою работу, его результат: `pow(2, 3) = 8`.</dd>
</dl>
Глубина рекурсии в данном случае составила: **3**.
Как видно из иллюстраций выше, глубина рекурсии равна максимальному числу контекстов, одновременно хранимых в стеке.
Обратим внимание на требования к памяти. Рекурсия приводит к хранению всех данных для неоконченных внешних вызовов в стеке, в данном случае это приводит к тому, что возведение в степень `n` хранит в памяти `n` различных контекстов.
Реализация возведения в степень через цикл гораздо более экономна:
```js
function pow(x, n) {
var result = x;
for (var i = 1; i < n; i++) {
result *= x;
}
return result;
}
```
У такой функции `pow` будет один контекст, в котором будут последовательно меняться значения `i` и `result`.
**Любая рекурсия может быть переделана в цикл. Как правило, вариант с циклом будет эффективнее.**
Но переделка рекурсии в цикл может быть нетривиальной, особенно когда в функции, в зависимости от условий, используются различные рекурсивные подвызовы, когда ветвление более сложное.
## Итого
Рекурсия -- это когда функция вызывает сама себя, как правило, с другими аргументами.
Существуют много областей применения рекурсивных вызовов. Здесь мы посмотрели на один из них -- решение задачи путём сведения её к более простой (с меньшими аргументами), но также рекурсия используется для работы с "естественно рекурсивными" структурами данных, такими как 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: "Consolas", monospace;
padding: 3px 5px;
}
.function-execution-context li:last-child {
font-weight: bold;
}
</style>
[/head]