Урок 18 из 49 · Месяц 3. JavaScript, часть 2

Регулярные выражения и рекурсия

Содержание урока

В этом уроке разберём две темы, которые часто пугают новичков, но на деле устроены просто. Регулярные выражения помогают искать и проверять текст по шаблону: email, номер телефона, пароль. Рекурсия — это приём, когда функция вызывает сама себя; с её помощью удобно обходить вложенные структуры вроде меню, комментариев с ответами или дерева папок.

Что такое регулярное выражение

Регулярное выражение (коротко — «регулярка», англ. regular expression, RegExp) — это шаблон для поиска в тексте. Обычный поиск ищет точное слово: «кот». Регулярка умеет искать по правилу: «пять цифр подряд», «слово, которое начинается с большой буквы», «что-то похожее на email».

Бытовая аналогия: вы ищете в телефонной книге не конкретного «Ивана», а «всех, чьё имя начинается на И и у кого номер заканчивается на 77». Это и есть шаблон.

Где регулярки встречаются во фронтенде:

  • проверка полей формы (email, телефон, пароль);
  • поиск и замена в тексте (убрать лишние пробелы, спрятать часть номера карты);
  • разбор строк (достать все хештеги из поста, все числа из строки).

Как создать регулярное выражение

В JavaScript есть два способа:

// 1. Литерал — шаблон между косыми чертами
const re1 = /кот/;

// 2. Конструктор — удобно, когда шаблон собирается из переменной
const word = "кот";
const re2 = new RegExp(word);

console.log(re1.test("Мой кот спит")); // true
console.log(re2.test("Собака лает"));  // false
  • /кот/ — самый частый способ. Всё, что между / и /, — шаблон.
  • new RegExp(word) — нужен, когда шаблон заранее неизвестен (например, пользователь ввёл слово для поиска).
  • .test(строка) — метод регулярки, который возвращает true, если в строке нашлось совпадение, и false — если нет.

Из чего состоит шаблон

Специальные символы

Большинство символов в шаблоне означают сами себя: /abc/ ищет буквально «abc». Но некоторые символы имеют особый смысл:

Символ Что означает Пример Совпадёт с
. любой один символ (кроме перевода строки) /к.т/ «кот», «кит», «к7т»
\d цифра 0–9 /\d\d/ «42» в «мне 42 года»
\w латинская буква, цифра или _ /\w+/ «user_1»
\s пробельный символ (пробел, табуляция, перевод строки) /a\sb/ «a b»
\D, \W, \S всё, кроме цифры / буквы / пробела /\D/ любой не-цифровой символ
^ начало строки /^Привет/ строка начинается с «Привет»
$ конец строки /\.js$/ строка заканчивается на «.js»

Если нужно найти сам специальный символ (например, точку), его экранируют обратной косой чертой: \. — это именно точка, а не «любой символ».

Наборы символов

Квадратные скобки означают «один любой символ из списка»:

/[аеиоу]/   // одна любая гласная из списка
/[0-9]/     // одна цифра (то же, что \d)
/[a-zA-Z]/  // одна латинская буква в любом регистре
/[^0-9]/    // ^ внутри скобок — «НЕ»: любой символ, кроме цифры

Квантификаторы — «сколько раз»

Квантификатор ставится после символа и говорит, сколько раз он может повториться:

Квантификатор Значение Пример
+ один или больше /\d+/ — одно или несколько чисел подряд
* ноль или больше /ab*/ — «a», «ab», «abbb»
? ноль или один (необязательный) /colou?r/ — «color» и «colour»
{n} ровно n раз /\d{4}/ — ровно 4 цифры
{n,m} от n до m раз /\d{2,4}/ — от 2 до 4 цифр
{n,} n и больше /\w{8,}/ — минимум 8 символов

Группы и «или»

  • Круглые скобки ( ) объединяют часть шаблона в группу: /(ха)+/ найдёт «ха», «хаха», «хахаха».
  • Вертикальная черта | означает «или»: /кот|пёс/ найдёт любое из слов.

Флаги

Флаги пишутся после закрывающей / и меняют поведение поиска:

Флаг Что делает
g global — искать все совпадения, а не только первое
i ignore case — не различать большие и маленькие буквы
m multiline — ^ и $ работают для каждой строки текста
u unicode — правильная работа с эмодзи и \p{...}
console.log(/привет/i.test("ПРИВЕТ, мир")); // true — регистр не важен

Методы для работы с регулярками

test — есть ли совпадение

const hasDigit = /\d/;
console.log(hasDigit.test("пароль"));   // false
console.log(hasDigit.test("пароль1"));  // true

Чаще всего test используется для проверки форм: «подходит ли ввод под правило?».

match и matchAll — найти совпадения

const text = "Цены: 150 сом, 320 сом и 75 сом";

console.log(text.match(/\d+/));   // ["150", index: 6, ...] — только первое
console.log(text.match(/\d+/g));  // ["150", "320", "75"] — все сразу
  • Без флага g метод match возвращает первое совпадение с подробностями (позиция, группы).
  • С флагом g — просто массив всех найденных строк.
  • Если ничего не нашлось, вернётся null. Поэтому перед .length или .map стоит проверить результат.

Когда нужны и все совпадения, и группы — используйте matchAll:

const log = "user:anna age:25, user:timur age:31";

for (const m of log.matchAll(/user:(\w+) age:(\d+)/g)) {
  console.log(m[1], m[2]); // anna 25, потом timur 31
}

m[0] — всё совпадение целиком, m[1] и m[2] — содержимое первой и второй круглых скобок.

replace и replaceAll — замена

const messy = "Привет,    как    дела?";
console.log(messy.replace(/\s+/g, " ")); // "Привет, как дела?"

const card = "4400 1234 5678 9012";
console.log(card.replace(/\d{4}(?= )/g, "****")); // "**** **** **** 9012"

Разбор:

  • /\s+/g — «один или больше пробелов подряд, везде». Каждую такую группу заменяем на один пробел.
  • \d{4}(?= ) — «четыре цифры, после которых идёт пробел». Конструкция (?= ) называется опережающей проверкой: она проверяет, что дальше пробел, но сам пробел в совпадение не попадает. Последние четыре цифры без пробела после них остаются видимыми.

split — разбить строку

const tags = "js, html ,css,react";
console.log(tags.split(/\s*,\s*/)); // ["js", "html", "css", "react"]

Шаблон \s*,\s* — «запятая, вокруг которой может быть сколько угодно пробелов». Так мы сразу избавляемся от лишних пробелов.

Практический пример: проверка формы

Соберём набор правил для формы регистрации:

const rules = {
  email: /^[\w.-]+@[\w-]+\.[a-z]{2,}$/i,
  phone: /^\+996\d{9}$/,
  password: /^(?=.*\d)(?=.*[A-Z]).{8,}$/,
};

function validate(field, value) {
  return rules[field].test(value.trim());
}

console.log(validate("email", "anna@mail.com"));     // true
console.log(validate("email", "anna@mail"));         // false
console.log(validate("phone", "+996700123456"));     // true
console.log(validate("password", "qwerty"));         // false
console.log(validate("password", "Qwerty123"));      // true

Разберём каждое правило:

  • email: ^ начало → [\w.-]+ одна или больше букв, цифр, точек или дефисов → @ → [\w-]+ имя домена → \. точка → [a-z]{2,} зона домена из двух и более букв → $ конец. Флаг i разрешает большие буквы.
  • phone: строка должна начинаться с +996 (плюс экранирован, потому что + — квантификатор) и дальше ровно 9 цифр.
  • password: (?=.*\d) — «где-то дальше есть цифра», (?=.*[A-Z]) — «где-то есть большая латинская буква», .{8,} — всего не меньше 8 символов.

Типичные ошибки с регулярками

  • Забыли экранировать точку, плюс или скобку: /site.com/ совпадёт и с «siteXcom».
  • Забыли флаг g в replace — заменится только первое совпадение.
  • Нет ^ и $ при проверке поля — проходит лишний мусор.
  • Слишком сложный шаблон в одну строку. Если регулярку сложно прочитать — разбейте проверку на несколько простых test.

Полный справочник — на MDN: регулярные выражения.

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

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

Аналогия — матрёшка. Чтобы найти самую маленькую куклу, вы открываете матрёшку. Внутри — снова матрёшка: повторяете то же действие. Когда внутри больше ничего нет — вы нашли ответ и останавливаетесь.

У любой рекурсивной функции есть две обязательные части:

  1. Базовый случай — условие остановки. Задача настолько простая, что ответ известен сразу.
  2. Рекурсивный шаг — функция вызывает себя с «уменьшенной» задачей, которая приближает нас к базовому случаю.

Первый пример: обратный отсчёт

function countdown(n) {
  if (n === 0) {           // базовый случай
    console.log("Старт!");
    return;
  }
  console.log(n);
  countdown(n - 1);        // рекурсивный шаг: та же задача, но n меньше
}

countdown(3); // 3, 2, 1, Старт!

Что происходит:

  • countdown(3) печатает 3 и вызывает countdown(2);
  • countdown(2) печатает 2 и вызывает countdown(1);
  • countdown(1) печатает 1 и вызывает countdown(0);
  • countdown(0) попадает в базовый случай, печатает «Старт!» и завершается.

Пример с возвратом значения: факториал

Факториал числа n (пишется n!) — это произведение всех чисел от 1 до n: 4! = 4 × 3 × 2 × 1 = 24. Заметьте: 4! = 4 × 3!. Задача сама сводится к меньшей.

function factorial(n) {
  if (n <= 1) return 1;          // базовый случай: 1! = 1
  return n * factorial(n - 1);   // n! = n × (n-1)!
}

console.log(factorial(4)); // 24

Как считается factorial(4):

factorial(4) = 4 * factorial(3)
             = 4 * (3 * factorial(2))
             = 4 * (3 * (2 * factorial(1)))
             = 4 * (3 * (2 * 1))
             = 24

Каждый вызов «ждёт» ответа от следующего. Когда дошли до factorial(1), ответы начинают возвращаться обратно по цепочке.

Стек вызовов

JavaScript запоминает незавершённые вызовы функций в стеке вызовов (call stack) — это как стопка тарелок: новый вызов кладётся сверху, а снимается первым тот, что лежит наверху. Если базового случая нет, тарелки кладутся бесконечно, и браузер выдаёт ошибку:

function broken(n) {
  return broken(n - 1); // нет условия остановки!
}

broken(5); // RangeError: Maximum call stack size exceeded

О стеке вызовов мы подробнее поговорим в уроке про Event Loop.

Где рекурсия по-настоящему полезна

Факториал легко посчитать и обычным циклом. Рекурсия раскрывается на вложенных данных, где заранее неизвестна глубина.

Обход вложенного меню

const menu = [
  { title: "Главная" },
  {
    title: "Каталог",
    children: [
      { title: "Телефоны" },
      {
        title: "Ноутбуки",
        children: [{ title: "Игровые" }, { title: "Для работы" }],
      },
    ],
  },
  { title: "Контакты" },
];

function printMenu(items, level = 0) {
  for (const item of items) {
    console.log("  ".repeat(level) + "- " + item.title);
    if (item.children) {
      printMenu(item.children, level + 1); // спускаемся на уровень глубже
    }
  }
}

printMenu(menu);

Результат:

- Главная
- Каталог
  - Телефоны
  - Ноутбуки
    - Игровые
    - Для работы
- Контакты

Разбор:

  • level — глубина вложенности. Для отступа повторяем два пробела level раз.
  • Если у пункта есть children, вызываем ту же функцию для дочерних пунктов, увеличив уровень.
  • Базовый случай здесь «спрятан»: когда у пункта нет children, новый вызов не происходит.

Сумма всех чисел во вложенном массиве

function deepSum(arr) {
  let sum = 0;
  for (const item of arr) {
    if (Array.isArray(item)) {
      sum += deepSum(item);   // элемент — массив: считаем его сумму рекурсивно
    } else {
      sum += item;            // элемент — число: просто прибавляем
    }
  }
  return sum;
}

console.log(deepSum([1, [2, 3], [4, [5, [6]]]])); // 21

Рекурсия или цикл?

Цикл Рекурсия
Плоский список, счётчик ✅ проще и быстрее можно, но излишне
Дерево, вложенные объекты сложно, нужен свой стек ✅ естественно и коротко
Очень большая глубина (десятки тысяч уровней) ✅ безопасно риск переполнить стек

Практика

  1. Проверка индекса. Напишите регулярку для почтового индекса из ровно 6 цифр. test("720001") → true, test("72001") → false, test("720001a") → false.
  2. Хештеги. Из строки "Учу #js и #html, скоро #react" достаньте массив ["#js", "#html", "#react"] с помощью match и флага g.
  3. Цензура. Напишите функцию hidePhone(str), которая в номере +996700123456 оставляет видимыми только последние 2 цифры: +996*******56. Подсказка: replace с функцией или опережающей проверкой.
  4. Сумма цифр. Напишите рекурсивную функцию sumDigits(n), которая считает сумму цифр числа: sumDigits(1234) → 10. Базовый случай — однозначное число. Подсказка: n % 10 и Math.floor(n / 10).
  5. Поиск в дереве. Используя массив menu из урока, напишите рекурсивную функцию findItem(items, title), которая возвращает пункт меню с нужным названием на любой глубине или null. findItem(menu, "Игровые") → { title: "Игровые" }.

Итоги

  • Регулярное выражение — шаблон для поиска и проверки текста: /шаблон/флаги.
  • Главные элементы: \d, \w, \s, ., наборы [...], квантификаторы + * ? {n,m}, якоря ^ и $.
  • Методы: test — проверить, match/matchAll — найти, replace — заменить, split — разбить.
  • Проверка на клиенте не заменяет проверку на сервере.
  • Рекурсия — функция, вызывающая саму себя; обязательно нужны базовый случай и шаг к нему.
  • Без базового случая получится RangeError: Maximum call stack size exceeded.
  • Рекурсия особенно удобна для деревьев и вложенных структур.
Отзыв