Чем больше вы кодируете, тем больше обнаруживаете, что существует не единственный способ решить проблему. Настоящее ремесло в кодировании - это научиться писать код, который не просто решает проблему, но делает это чистым, красноречивым и читаемым способом. Эффектный и красноречивый паттерн - это рекурсия.

Что такое рекурсия

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

Обычный способ познакомить людей с рекурсией - это факторный расчет. Если вы не помните, что такое факториал, факториал любого положительного действительного числа - это умножение всех действительных чисел от 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

Рекурсия может быть трудной для понимания, особенно поначалу. Если все сделано правильно, рекурсия - это мощный инструмент, который позволяет писать чистый и элегантный код, позволяющий делать удивительные вещи.