Урок 18 из 49 · Месяц 3. JavaScript, часть 2
Регулярные выражения и рекурсия
Содержание урока
- Что такое регулярное выражение
- Как создать регулярное выражение
- Из чего состоит шаблон
- Специальные символы
- Наборы символов
- Квантификаторы — «сколько раз»
- Группы и «или»
- Флаги
- Методы для работы с регулярками
- test — есть ли совпадение
- match и matchAll — найти совпадения
- replace и replaceAll — замена
- split — разбить строку
- Практический пример: проверка формы
- Типичные ошибки с регулярками
- Что такое рекурсия
- Первый пример: обратный отсчёт
- Пример с возвратом значения: факториал
- Стек вызовов
- Где рекурсия по-настоящему полезна
- Обход вложенного меню
- Сумма всех чисел во вложенном массиве
- Рекурсия или цикл?
- Практика
- Итоги
В этом уроке разберём две темы, которые часто пугают новичков, но на деле устроены просто. Регулярные выражения помогают искать и проверять текст по шаблону: 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: регулярные выражения.
Что такое рекурсия
Рекурсия — это когда функция вызывает саму себя, чтобы решить задачу по частям. Большую задачу сводим к такой же, но поменьше, — и так до тех пор, пока задача не станет совсем простой.
Аналогия — матрёшка. Чтобы найти самую маленькую куклу, вы открываете матрёшку. Внутри — снова матрёшка: повторяете то же действие. Когда внутри больше ничего нет — вы нашли ответ и останавливаетесь.
У любой рекурсивной функции есть две обязательные части:
- Базовый случай — условие остановки. Задача настолько простая, что ответ известен сразу.
- Рекурсивный шаг — функция вызывает себя с «уменьшенной» задачей, которая приближает нас к базовому случаю.
Первый пример: обратный отсчёт
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
Рекурсия или цикл?
| Цикл | Рекурсия | |
|---|---|---|
| Плоский список, счётчик | ✅ проще и быстрее | можно, но излишне |
| Дерево, вложенные объекты | сложно, нужен свой стек | ✅ естественно и коротко |
| Очень большая глубина (десятки тысяч уровней) | ✅ безопасно | риск переполнить стек |
Практика
- Проверка индекса. Напишите регулярку для почтового индекса из ровно 6 цифр.
test("720001")→true,test("72001")→false,test("720001a")→false. - Хештеги. Из строки
"Учу #js и #html, скоро #react"достаньте массив["#js", "#html", "#react"]с помощьюmatchи флагаg. - Цензура. Напишите функцию
hidePhone(str), которая в номере+996700123456оставляет видимыми только последние 2 цифры:+996*******56. Подсказка:replaceс функцией или опережающей проверкой. - Сумма цифр. Напишите рекурсивную функцию
sumDigits(n), которая считает сумму цифр числа:sumDigits(1234)→10. Базовый случай — однозначное число. Подсказка:n % 10иMath.floor(n / 10). - Поиск в дереве. Используя массив
menuиз урока, напишите рекурсивную функциюfindItem(items, title), которая возвращает пункт меню с нужным названием на любой глубине илиnull.findItem(menu, "Игровые")→{ title: "Игровые" }.
Итоги
- Регулярное выражение — шаблон для поиска и проверки текста:
/шаблон/флаги. - Главные элементы:
\d,\w,\s,., наборы[...], квантификаторы+ * ? {n,m}, якоря^и$. - Методы:
test— проверить,match/matchAll— найти,replace— заменить,split— разбить. - Проверка на клиенте не заменяет проверку на сервере.
- Рекурсия — функция, вызывающая саму себя; обязательно нужны базовый случай и шаг к нему.
- Без базового случая получится
RangeError: Maximum call stack size exceeded. - Рекурсия особенно удобна для деревьев и вложенных структур.