Чем больше вы кодируете, тем больше обнаруживаете, что существует не единственный способ решить проблему. Настоящее ремесло в кодировании - это научиться писать код, который не просто решает проблему, но делает это чистым, красноречивым и читаемым способом. Эффектный и красноречивый паттерн - это рекурсия.
Что такое рекурсия
Рекурсия - это способ перебрать проблему путем многократного вызова самой функции (да, я сказал сам вызов) до тех пор, пока не будет найден результат. Представление о функции, способной вызывать саму себя, может показаться ошеломляющим, но это очень эффективный способ описания проблемы.
Обычный способ познакомить людей с рекурсией - это факторный расчет. Если вы не помните, что такое факториал, факториал любого положительного действительного числа - это умножение всех действительных чисел от 1 до этого числа. В начальной школе вас учили изображать факториал с восклицательным знаком. Итак, !3
это 3*2*1
, что равно 6. !10
это 10*9*8*7*6*5*4*3*2*1
, что составляет 3 628 800.
Если бы я попросил вас написать функцию, которая вычисляет факториал заданного значения, вы, вероятно, заметили бы, что решение проблемы состоит в том, чтобы постепенно повторять один и тот же шаг умножения снова и снова, или, другими словами, решение зацикливается. Учитывая это знание, вы можете написать что-то вроде этого (для простоты я предполагаю, что в эту функцию будут передаваться только неотрицательные целые числа):
function factorial(num){ let result = 1; for(let i = 1;i<=num;i++){ result *= i; } return result; } factorial(3) //6
Это работает, но не так красноречиво, как могло бы быть, и вынуждает воспользоваться моментом, когда вы читаете код, чтобы понять, что происходит.
Давайте посмотрим на эту проблему с другой стороны. !0
это 1 и !1
также 1. !2
также можно записать как 2*!1
, что то же самое, что и 2*1
, потому что !1 === 1
. Все еще со мной? А как насчет 3? !3
можно переписать как 3*!2
, потому что !2 === 2*!1
то же самое, что и 2*1.
Итак, чтобы подвести итог, этот шаблон !n
- это просто n*!(n-1)
, где n - это число, для которого вы пытаетесь вычислить факториал. Так как бы мы выразили это в коде? Вот один из способов написать это:
function factorial(num){ if(num === 1 || num === 0) return 1; return num * factorial(num - 1); } factorial(3) //6
Давайте разберем этот код. Первая строка функции проверяет, равно ли значение 1 или 0, и возвращает ли оно значение. Это то, что обычно называют «базовым случаем» или случаем, когда вы отрываете от остальной части кода и больше не вызываете другую функцию.
Обычно сначала пишут базовый вариант, прежде чем писать остальную логику, чтобы не зацикливаться на бесконечном цикле. Также принято помещать базовый вариант в начало функции, как я там написал.
Остальное просто следует логике, которую мы уже описали. Умножение числа на факториал числа -1. Вы видите, насколько это чище? Это не только удалило весь цикл, но и необходимость поддерживать состояние результата на каждой итерации цикла.
Проблема реального мира
Одно дело - код вычислять факториал числа, но кто пытается использовать JavaScript для этого? Какая реальная проблема решается с помощью рекурсии?
Возьмем предметы. В предыдущем посте об объявлении переменных я объяснил, что переменные, определенные с помощью const
, не могут быть переназначены. Важно, чтобы я использовал слово переназначен, а не изменен, потому что объекты, функции и массивы нельзя переназначить, но их можно изменить. Возьмем, к примеру, следующее:
const obj = { name : "Bob" }; obj = { name = "Sam" } //this will throw an error obj.name = "Sam" //no error
Это связано с тем, что свойства объекта все еще можно изменить, даже если переменной нельзя переназначить другое значение.
В JavaScript Object имеет статический метод с именем freeze
, который позволяет «заморозить» каждое из свойств объекта, чтобы эти свойства больше нельзя было переназначить. Теперь мы можем это сделать:
const obj = { name : "Bob" }; Object.freeze(obj) obj.name = "Sam" //This now throws an error
Проблема с Object.freeze
заключается в том, что он выполняет только поверхностное замораживание, что означает, что он только замораживает свойства переданного объекта. Если одному из свойств был назначен другой объект в качестве его значения, свойства в этом объекте не замораживаются, как это :
const obj = { name: "Bob" job = { title : "Worker" } }; Object.freeze(obj); obj.job.title = "Manager" //no error
Что нам нужно сделать, так это написать какую-нибудь функцию «глубокого замораживания». К счастью, вложенные объекты - идеальный вариант использования рекурсии.
Давайте сначала обдумаем проблему. Учитывая значение, мы хотим определить, является ли это объектом. Если это так, мы хотим перебрать его свойства и заморозить их, если они являются объектами, а затем заморозить сам объект. В противном случае ничего не делайте.
Таким образом, базовый случай нашей deepFreeze
функции определяет, является ли значение, переданное в функцию, объектом, и если это не так, ничего не делать.
function deepFreeze(obj){ if(typeof obj !== 'object') return; }
Хорошо, теперь у нас есть базовый набор. Шаг второй - перебрать значения свойств и заморозить их, если они тоже являются объектами. Это упрощается с помощью статического метода Object.values
, который принимает объект и возвращает массив всех значений.
Учитывая, что это массив, мы можем затем вызвать метод forEach
, который принимает функцию обратного вызова, которая вызывается для каждого из значений. Вы можете догадаться, какую функцию мы собираемся передать в качестве обратного вызова? Точно, функция deepFreeze - идеальная функция для передачи в нее, например:
function deepFreeze(obj){ if(typeof obj !== 'object') return; Object.values(obj).forEach(deepFreeze) }
Теперь все, что нам нужно сделать, это заморозить сам объект, и все готово:
function deepFreeze(obj){ if(typeof obj !== 'object') return; Object.values(obj).forEach(deepFreeze) Object.freeze(obj); }
Теперь мы можем это сделать и получить ожидаемый результат:
const obj = { name: "Bob" job = { title : "Worker" } }; deepFreeze(obj); obj.job.title = "Manager" //This now throws an error
Рекурсия может быть трудной для понимания, особенно поначалу. Если все сделано правильно, рекурсия - это мощный инструмент, который позволяет писать чистый и элегантный код, позволяющий делать удивительные вещи.