Решение
Задача на разворот слов — простая, но требует внимательности к деталям. Покажем несколько способов реализации.
Решение 1: map + split/join (самый читаемый)
Способ с использованием функциональных методов:
function reverseWords(str: string): string {
return str
.split(" ") // Разбиваем на слова
.map(word => // Разворачиваем каждое слово
word.split("").reverse().join("")
)
.join(" "); // Собираем обратно
}
console.log(reverseWords("Hello World"));
// "olleH dlroW"
console.log(reverseWords("The quick brown fox"));
// "ehT kciuq nworb xof"
console.log(reverseWords("a b c"));
// "a b c"
console.log(reverseWords("JavaScript"));
// "tpircSavaJ"
Анализ:
Решение: Группировка массива объектов по ключу
Практическое применение
Группировка данных — один из самых часто используемых операций при обработке массивов. Она применяется при фильтрации по категориям, агрегировании статистики, организации данных для отображения. Эта функция работает за один проход массива с O(n) сложностью.
Базовое решение
function groupBy(arr, key) {
return arr.reduce((result, item) => {
const groupKey = item[key];
if (!result[groupKey]) {
result[groupKey] = [];
}
result[groupKey].push(item);
return result;
}, {});
}
Версия с поддержкой функции
Решение
Задача на бесконечный скролл с Intersection Observer API — показывает понимание производительности и современных браузерных API.
Решение 1: Базовый бесконечный скролл
import React, { useState, useEffect, useRef, useCallback } from "react";
interface InfiniteScrollProps<T> {
loadMore: (page: number) => Promise<T[]>;
renderItem: (item: T, index: number) => React.ReactNode;
hasMore?: boolean;
offset?: number;
}
function InfiniteScroll<T>({
loadMore,
renderItem,
hasMore = true,
offset = 500,
}: InfiniteScrollProps<T>) {
const [items, setItems] = useState<T[]>([]);
const [page, setPage] = useState(0);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
const observerTarget = useRef<HTMLDivElement>(null);
const loadingRef = useRef(false);
// Загрузка данных
const handleLoadMore = useCallback(async () => {
if (loadingRef.current || !hasMore) return;
Решение
Задача на функцию curry демонстрирует функциональное программирование.
Решение 1: Базовый curry
function curry(fn) {
const arity = fn.length;
return function curried(...args) {
if (args.length >= arity) {
return fn(...args);
}
return (...nextArgs) => curried(...args, ...nextArgs);
};
}
function sum(a, b, c) {
return a + b + c;
}
const curriedSum = curry(sum);
console.log(curriedSum(1)(2)(3)); // 6
console.log(curriedSum(1, 2)(3)); // 6
Как работает
Практический пример
function fetchAPI(baseURL, headers, endpoint, query) {
const url = new URL(endpoint, baseURL);
return fetch(url.toString(), { headers });
}
Решение: Реализация функции throttle
Различие между throttle и debounce
В отличие от debounce, который откладывает вызов до конца периода спокойствия, throttle выполняет функцию максимум один раз за заданный интервал времени. Это критично при обработке часто возникающих событий типа scroll или mousemove, где нужна регулярная обработка без перегрузки.
Базовая реализация
function throttle(fn, interval) {
let lastCallTime = 0;
return function throttled(...args) {
const now = Date.now();
if (now - lastCallTime >= interval) {
lastCallTime = now;
fn.apply(this, args);
}
};
}
Версия с трейлингим вызовом
Решение: Реализация Promise.all
Понимание задачи
Promise.all — это фундаментальный метод для работы с несколькими асинхронными операциями одновременно. Он ждёт выполнения всех промисов и возвращает единственный промис с массивом результатов в исходном порядке. Это критически важно для сценариев, где нужны результаты нескольких асинхронных вызовов перед дальнейшей обработкой.
Базовая реализация
Решение
Задача на создание хука useDebounce — классическая для показа понимания асинхронности, эффектов и очистки ресурсов в React. Создадим несколько версий.
Решение 1: Базовый useDebounce
Простая и понятная реализация:
import { useState, useEffect } from "react";
function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value);
useEffect(() => {
// Устанавливаем таймер
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Очищаем таймер при изменении value или размонтировании
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
export default useDebounce;
Решение
Задача на подсчёт гласных букв — простая на первый взгляд, но имеет несколько интересных вариантов реализации. Покажем разные подходы.
Решение 1: Регулярные выражения
Самый компактный и понятный способ:
function countVowels(str: string): number {
// Регулярное выражение: ищем гласные (любой регистр)
const vowels = str.match(/[aeiou]/gi);
return vowels ? vowels.length : 0;
}
console.log(countVowels("hello")); // 2
console.log(countVowels("AEIOU")); // 5
console.log(countVowels("rhythm")); // 0
console.log(countVowels("The quick brown fox")); // 5
console.log(countVowels("")); // 0
Анализ:
[aeiou] — класс символов, гласные английского алфавитаg флаг — глобальный поиск (все совпадения)i флаг — игнорировать регистрРешение 2: Цикл с массивом
Отсутствие зависимостей от regex:
Решение
Задача на проверку палиндрома — классическая в собеседованиях. Требует понимания строк, регулярных выражений и оптимизации по памяти.
Базовое решение
Сначала решим задачу стандартным способом:
function isPalindrome(str: string): boolean {
// Удаляем все символы, кроме букв и цифр
// Преобразуем в нижний регистр
const cleaned = str
.toLowerCase()
.replace(/[^a-z0-9]/g, "");
// Проверяем, совпадает ли строка с обратной версией
return cleaned === cleaned.split("").reverse().join("");
}
Анализ:
toLowerCase() — игнорируем регистрreplace(/[^a-z0-9]/g, "") — убираем всё кроме букв и цифрОптимизированное решение (два указателя)
Для бонуса решим без создания дополнительной строки:
Решение функции debounce
Что такое debounce?
Debounce — это техника оптимизации, которая откладывает выполнение функции до тех пор, пока не пройдёт определённое время с момента последнего вызова. Это критически важно при обработке часто вызываемых событий (изменение input, скролл, resize), чтобы избежать множественных ненужных вычислений.
Реализация базовой версии
function debounce(fn, delay) {
let timeoutId;
return function debounced(...args) {
// Очищаем предыдущий таймер
clearTimeout(timeoutId);
// Устанавливаем новый таймер
timeoutId = setTimeout(() => {
fn.apply(this, args);
}, delay);
};
}
С методом cancel() и улучшениями
Решение
Эта классическая задача на понимание замыканий, области видимости (var vs let) и асинхронного выполнения JavaScript.
Что выведет код?
for (var i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// Результат: 3 3 3
Почему 3, 3, 3?
Способ 1: Использовать let вместо var
for (let i = 0; i < 3; i++) {
setTimeout(function() {
console.log(i);
}, 1000);
}
// Результат: 0 1 2
Почему работает:
Способ 2: Замыкание через IIFE
Решение
Задача на валидирующую форму регистрации — классическая, показывает понимание управления состоянием и валидации в React. Создадим полное решение.
Решение 1: Базовая форма с валидацией
import React, { useState, useMemo } from "react";
interface FormErrors {
email?: string;
password?: string;
confirmPassword?: string;
username?: string;
}
interface FormData {
email: string;
password: string;
confirmPassword: string;
username: string;
}
function RegistrationForm() {
const [formData, setFormData] = useState<FormData>({
email: "",
password: "",
confirmPassword: "",
username: "",
});
// Валидация
const errors = useMemo<FormErrors>(() => {
const newErrors: FormErrors = {};
Решение
FizzBuzz — классическая задача на собеседованиях, проверяющая логику условий и способность обобщать код. Решим её в несколько уровней сложности.
Базовое решение
Прямолинейный подход с условными операторами:
function fizzBuzz(n: number): string[] {
const result: string[] = [];
for (let i = 1; i <= n; i++) {
// Проверяем делимость на 15 первой (это и 3, и 5)
if (i % 15 === 0) {
result.push("FizzBuzz");
} else if (i % 3 === 0) {
result.push("Fizz");
} else if (i % 5 === 0) {
result.push("Buzz");
} else {
result.push(i.toString());
}
}
return result;
}
console.log(fizzBuzz(15).join(", "));
// 1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz, 11, Fizz, 13, 14, FizzBuzz
Анализ:
Улучшенное решение: Конкатенация
Более элегантный способ без многоуровневых условий:
Решение
Задача на проверку анаграмм — проверяет понимание структур данных и алгоритмов сортировки. Это часто встречается на собеседованиях, так как показывает знание оптимизации.
Решение 1: Сортировка
Самый интуитивный подход — отсортировать буквы и сравнить:
function isAnagram(str1: string, str2: string): boolean {
// Очищаем строки: удаляем пробелы, приводим к нижнему регистру
const clean1 = str1.toLowerCase().replace(/\s/g, "");
const clean2 = str2.toLowerCase().replace(/\s/g, "");
// Проверяем длину
if (clean1.length !== clean2.length) {
return false;
}
// Сортируем буквы и сравниваем
const sorted1 = clean1.split("").sort().join("");
const sorted2 = clean2.split("").sort().join("");
return sorted1 === sorted2;
}
Анализ:
Решение 2: Подсчёт частоты (оптимальное)
Решение: Развертывание вложенного объекта в плоскую структуру
Задача и её применение
Развёртывание вложенных объектов часто требуется при работе с конфигурациями, сохранением в базы данных, экспортом в CSV и прочих сценариях, где нужна плоская структура данных. Эта функция использует рекурсию для обхода всех уровней вложенности.
Базовое решение
function flattenObject(obj, prefix = '') {
const result = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
const value = obj[key];
const newKey = prefix ? `${prefix}.${key}` : key;
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
Object.assign(result, flattenObject(value, newKey));
} else {
result[newKey] = value;
}
}
}
return result;
}
Версия с поддержкой массивов
Решение: Компонент бесконечного скролла на React
Базовое решение с Intersection Observer
Компонент InfiniteScroll использует IntersectionObserver для определения когда пользователь прокрутил к концу списка. При этом вызывается callback onLoadMore для загрузки новых данных.
Ключевые компоненты
Intersection Observer: observerTarget ref отслеживает видимость элемента в конце списка. Когда элемент становится видимым, вызывается handleLoadMore.
Защита от дублей: isLoadingRef предотвращает множественные одновременные запросы. Проверяем флаг перед загрузкой.
rootMargin: параметр offset задаёт расстояние в пиксель до конца списка, при котором нужно загружать (предзагрузка).
Индикаторы: отображаем спиннер при загрузке, сообщение об ошибке, кнопку Retry и сообщение о конце списка.
Использование
Решение
Задача на React-компонент таблицы с сортировкой — показывает понимание хуков, состояния и работы с данными. Создадим несколько версий.
Решение 1: Базовая таблица с сортировкой
import React, { useState, useMemo } from "react";
interface Column<T> {
key: keyof T;
label: string;
}
interface SortableTableProps<T extends Record<string, any>> {
data: T[];
columns: Column<T>[];
}
type SortOrder = "asc" | "desc" | null;
function SortableTable<T extends Record<string, any>>({
data,
columns,
}: SortableTableProps<T>) {
const [sortKey, setSortKey] = useState<keyof T | null>(null);
const [sortOrder, setSortOrder] = useState<SortOrder>(null);
Решение
Задача на числа Фибоначчи с мемоизацией — классическая для демонстрации понимания рекурсии, оптимизации и сложности алгоритмов. Это часто спрашивают на собеседованиях.
Проблема: Наивная рекурсия
Без оптимизации это работает очень медленно:
// ❌ НЕЭФФЕКТИВНО: O(2^n)
function fibonacciNaive(n: number): number {
if (n <= 1) return n;
return fibonacciNaive(n - 1) + fibonacciNaive(n - 2);
}
// fibonacci(50) займет вечность! Будет 2^50 вызовов
// fibonacci(20) уже ~ 1 миллион вызовов
Почему медленно: Одно и то же значение вычисляется много раз. Например, fibonacci(5) вычисляет fibonacci(3) дважды, fibonacci(2) трижды и т.д.
Решение 1: Мемоизация (Caching)
Запоминаем результаты вычислений:
Решение: Реализация Array.prototype.map
Понимание map
Метод map — один из столпов функционального программирования в JavaScript. Он создаёт новый массив, применяя функцию преобразования к каждому элементу исходного массива. Понимание внутреннего устройства помогает глубже освоить работу с данными.
Базовая реализация
function myMap(arr, callback) {
const result = [];
for (let i = 0; i < arr.length; i++) {
result.push(callback(arr[i], i, arr));
}
return result;
}
С поддержкой thisArg
function myMap(arr, callback, thisArg) {
const result = [];
for (let i = 0; i < arr.length; i++) {
// Вызываем callback с заданным контекстом
result.push(callback.call(thisArg, arr[i], i, arr));
}
return result;
}
TypeScript версия
Решение
Задача на валидацию email — показывает понимание регулярных выражений и строк.
Решение 1: С регулярным выражением
function isValidEmail(email) {
const regex = /^[a-zA-Z0-9._+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
return regex.test(email);
}
// Использование
console.log(isValidEmail("test@example.com")); // true
console.log(isValidEmail("user.name@domain.org")); // true
console.log(isValidEmail("invalid")); // false
console.log(isValidEmail("@nodomain.com")); // false
console.log(isValidEmail("test@.com")); // false
console.log(isValidEmail("test@domain")); // false
Объяснение regex:
^ — начало строки[a-zA-Z0-9._+-]+ — одна или больше букв, цифр, точка, подчёркивание, плюс, дефис@ — обязательный символ @[a-zA-Z0-9.-]+ — домен с буквами, цифрами, точками, дефисом\. — обязательная точка[a-zA-Z]{2,} — минимум 2 буквы после точки (TLD)$ — конец строкиРешение
Задача на адаптивное навигационное меню — показывает понимание responsive design, accessibility и работы с состоянием. Создадим несколько версий.
Решение 1: React с Tailwind CSS
import React, { useState } from "react";
interface NavLink {
label: string;
href: string;
icon?: string;
}
interface NavbarProps {
brand: string;
links: NavLink[];
currentPath?: string;
}
function Navbar({ brand, links, currentPath = "/" }: NavbarProps) {
const [isOpen, setIsOpen] = useState(false);
const toggleMenu = () => {
setIsOpen(!isOpen);
};
const closeMenu = () => {
setIsOpen(false);
};
Решение: Объединение перекрывающихся интервалов
Концепция и подход
Эта классическая задача часто встречается в интервьюшах и имеет элегантное решение. Ключевая идея — отсортировать интервалы по началу, затем пройти по ним один раз, объединяя те, которые перекрываются. Сложность O(n log n) достигается за счёт сортировки.
Базовая реализация
Решение
Задача на простой роутер — показывает понимание History API, регулярных выражений и управления состоянием приложения.
Решение 1: Базовый роутер
class Router {
constructor() {
this.routes = [];
this.current = null;
this.init();
}
add(path, callback) {
this.routes.push({ path, callback });
}
navigate(path) {
window.history.pushState({}, "", path);
this.handleRoute(path);
}
match(path) {
for (let route of this.routes) {
const regex = this.pathToRegex(route.path);
const match = regex.exec(path);
if (match) {
const params = this.extractParams(route.path, match);
return { callback: route.callback, params };
}
}
return null;
}
pathToRegex(path) {
const pattern = path
.replace(/\//g, "\\/')
.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, "([^/]+)");
return new RegExp("^" + pattern + "$");
}
Решение: Найти уникальный элемент в массиве
Проблема и подход
Эта задача демонстрирует мощь побитовых операций для оптимального решения. Наивный подход с хеш-таблицей требует O(n) пространства, но побитовая операция XOR позволяет решить за O(1) памяти благодаря уникальному свойству этой операции.
Почему XOR работает?
Операция XOR (исключающее ИЛИ) имеет уникальное свойство:
a ^ a = 0 (число XOR с самим собой равно нулю)a ^ 0 = a (число XOR с нулём равно самому числу)a ^ b = b ^ a (XOR коммутативна)(a ^ b) ^ c = a ^ (b ^ c) (XOR ассоциативна)Поэтому если XOR-ить все числа массива, все пары исчезнут (станут 0), и останется только уникальное число.
Оптимальное решение с XOR
function findUnique(arr) {
return arr.reduce((result, num) => result ^ num, 0);
}
Развёрнутая версия с пояснениями
Решение: Глубокое клонирование объекта
Почему глубокое клонирование важно?
При работе со сложными структурами данных в JavaScript обычное присваивание создаёт ссылку на объект, а не его копию. Изменение значений во вложенных объектах или массивах повлияет на оригинал. Глубокое клонирование создаёт полностью независимую копию, включая все вложенные структуры.
Базовая рекурсивная реализация
function deepClone(obj) {
// Обработка примитивов
if (obj === null || typeof obj !== "object") {
return obj;
}
// Обработка Date
if (obj instanceof Date) {
return new Date(obj.getTime());
}
// Обработка Array
if (Array.isArray(obj)) {
return obj.map(item => deepClone(item));
}
// Обработка Object
const cloned = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
cloned[key] = deepClone(obj[key]);
}
}
return cloned;
}
Версия с обработкой циклических ссылок
Решение
Задача на бинарный поиск — классическая для показа понимания алгоритмов и оптимизации.
Решение 1: Итеративный бинарный поиск
function binarySearch(arr, target) {
let left = 0;
let right = arr.length - 1;
while (left <= right) {
const mid = Math.floor((left + right) / 2);
const midValue = arr[mid];
if (midValue === target) {
return mid;
} else if (midValue < target) {
left = mid + 1; // Ищем справа
} else {
right = mid - 1; // Ищем слева
}
}
return -1; // Не найдено
}
// Тесты
console.log(binarySearch([1, 2, 3, 4, 5, 6, 7, 8, 9], 5)); // 4
console.log(binarySearch([1, 2, 3, 4, 5], 1)); // 0
console.log(binarySearch([1, 2, 3, 4, 5], 5)); // 4
console.log(binarySearch([1, 2, 3, 4, 5], 6)); // -1
console.log(binarySearch([], 1)); // -1
Решение
Задача на создание хука useFetch — показывает понимание работы с API, управления состоянием и жизненным циклом компонента. Создадим несколько версий.
Решение 1: Базовый useFetch
Простая реализация с основной функциональностью:
import { useState, useEffect, useRef } from "react";
interface UseFetchResult<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
function useFetch<T>(url: string): UseFetchResult<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const abortControllerRef = useRef<AbortController | null>(null);
useEffect(() => {
// Создаём AbortController для отмены запроса
abortControllerRef.current = new AbortController();
const fetchData = async () => {
try {
setLoading(true);
setError(null);
Решение: Самая длинная подстрока без повторений
Концепция скользящего окна
Эта задача идеально решается техникой скользящего окна (sliding window). Идея в том, что мы используем два указателя (left и right) для создания окна, которое расширяется и сжимается, всегда сохраняя условие уникальности символов. Это позволяет решить задачу за один проход.
Базовое решение
Решение
Задача на pipe и compose — демонстрирует функциональное программирование и преобразование функций.
Что такое pipe и compose?
pipe — слева направо
compose — справа налево
Решение 1: Базовые версии
const pipe = (...fns) => (x) =>
fns.reduce((acc, fn) => fn(acc), x);
const compose = (...fns) => (x) =>
fns.reduceRight((acc, fn) => fn(acc), x);
// Использование
const add5 = x => x + 5;
const multiply2 = x => x * 2;
const subtract3 = x => x - 3;
const piped = pipe(add5, multiply2, subtract3);
console.log(piped(10)); // ((10 + 5) * 2) - 3 = 27
const composed = compose(subtract3, multiply2, add5);
console.log(composed(10)); // ((10 + 5) * 2) - 3 = 27
Решение 2: С типизацией TypeScript
type Fn<T, R> = (arg: T) => R;
function pipe<T, R>(...fns: Fn<any, any>[]): (arg: T) => R {
return (x: T) => fns.reduce((acc, fn) => fn(acc), x);
}
Решение: Максимальная сумма подмассива (Алгоритм Кадана)
История и значение
Алгоритм Кадана, разработанный Джеем Кадейном в 1977 году, — один из классических примеров динамического программирования. Он решает задачу о максимальной сумме подмассива за линейное время, что считается оптимальным решением для этой проблемы.
Основная идея
Алгоритм поддерживает две переменные:
currentSum — максимальная сумма подмассива, заканчивающегося в текущей позицииmaxSum — глобально максимальная сумма найденная до текущего моментаНа каждой итерации принимаем решение: либо расширить текущий подмассив, включив текущий элемент, либо начать новый подмассив с текущего элемента.
Базовое решение