Практические задачи
Практические задачи на алгоритмы и замыкания — массивы, строки, рекурсия, цепочки методов и предсказание вывода.
19 вопросов
JuniorКодОчень частоСделать фабрику счётчиков на замыкании
Сделать фабрику счётчиков на замыкании
Объявить локальную let count = 0 внутри createCounter, затем вернуть объект, чьи методы inc и get замыкаются над этой переменной. Возвращённые методы хранят живую ссылку на count после возврата фабрики, поэтому она остаётся приватной. Каждый вызов фабрики создаёт свежий count, давая независимые счётчики.
Типичные ошибки
- ✗Класть
countна возвращаемый объект публичным полем вместо замкнутой локальной - ✗Думать, что все счётчики делят один счётчик, раз делят фабричную функцию
- ✗Считать, что
varдаёт блочную область — у неё функциональная, а не блочная
Уточняющие вопросы
- →Почему каждый вызов
createCounterпорождает независимыйcount? - →Как добавить метод
reset(), замыкающийся над той же переменной?
JuniorКодОчень частоЧто выведет этот фрагмент на порядок событийного цикла и почему?
Что выведет этот фрагмент на порядок событийного цикла и почему?
Печатает A, D, C, B. Два синхронных console.log выполняются первыми, поэтому A, затем D. Колбэк Promise.then — микрозадача, а колбэк setTimeout — макрозадача; после завершения синхронного кода все микрозадачи опустошаются раньше любой макрозадачи, поэтому C печатается до B, хотя задержка таймера 0.
Типичные ошибки
- ✗Думать, что колбэки срабатывают в порядке исходника, а не по типу очереди
- ✗Считать, что
setTimeout(…, 0)выполняется раньше ожидающих микрозадач - ✗Полагать, что движок блокируется на
setTimeoutили промисе на месте
Уточняющие вопросы
- →Где в этом порядке находятся
queueMicrotaskи продолжения послеawait? - →Изменит ли добавление второго
.thenместо печатиB?
SeniorКодОчень частоРеализовать debounce(fn, delay)
Реализовать debounce(fn, delay)
Хранить timeoutId в замыкании. Возвращённая функция вызывает clearTimeout(timeoutId), отменяя ожидающий запуск, затем timeoutId = setTimeout(...), планируя fn через delay. Внутри таймера вызвать fn.apply(this, args), чтобы сохранить this и передать аргументы. Выживает лишь таймер последнего вызова, поэтому fn запускается один раз после прекращения активности.
Типичные ошибки
- ✗Путать debounce с throttle — debounce ждёт паузу, а не фиксированную частоту
- ✗Забывать
clearTimeout, из-за чего каждый вызов срабатывает после своей задержки, а не только последний - ✗Терять
this/аргументы, вызываяfn()напрямую вместоfn.apply(this, args)
Уточняющие вопросы
- →Как добавить опцию переднего фронта, чтобы срабатывать и на первом вызове?
- →Как замкнутый
timeoutIdвыживает между отдельными вызовами?
JuniorКодЧастоСделать плоским произвольно вложенный массив
Сделать плоским произвольно вложенный массив
Пройти массив через reduce; для каждого элемента, если Array.isArray истинно, рекурсивно зайти внутрь и склеить плоский результат, иначе добавить значение. Именно рекурсия достаёт до любой глубины. Пустой массив сводится к [], а порядок элементов сохраняется на всех уровнях.
Типичные ошибки
- ✗Делать плоским только один уровень (голый
concat) и упускать глубже вложенное - ✗Проверять
typeof x === 'object'вместоArray.isArray, путаясь сnullи объектами - ✗Изменять входной массив во время рекурсии вместо построения нового
Уточняющие вопросы
- →Как сделать плоским только до фиксированной глубины
n, а не до самого дна? - →Почему
Array.isArrayбезопаснее, чемinstanceof Array, между контекстами?
JuniorКодЧастоЧто выведет этот фрагмент на приведение типов и почему?
Что выведет этот фрагмент на приведение типов и почему?
Строки печатают '33', 6, 4 и '[object Object]'. + с любой строкой склеивает, поэтому (1 + 2) + '3' даёт '33'. * и - принуждают к числам, поэтому '3' * 2 это 6, а '5' - 1 это 4. [] приводится к строке '', а {} — к '[object Object]', давая '[object Object]'.
Типичные ошибки
- ✗Думать, что
+всегда складывает — с любым строковым операндом он склеивает - ✗Считать, что
-и*склеивают строки, как+ - ✗Ожидать, что
{}приводится к'{}', а не к'[object Object]'
Уточняющие вопросы
- →Почему
+в1 + 2 + '3'вычисляется строго слева направо? - →Почему
[] + {}даёт'[object Object]', а не ошибку?
JuniorДебаггингЧастоЧто выведет этот сниппет с мутацией и переприсваиванием и почему?
Что выведет этот сниппет с мутацией и переприсваиванием и почему?
{ a: 5, b: 2 }, затем { a: 5, b: 2 }, затем 5. JavaScript передаёт ссылку на объект по значению: fn1 мутирует общий объект через параметр, поэтому изменение видно снаружи. fn3 лишь переприсваивает свой локальный параметр в 5 — это перепривязывает локальное имя, не трогая внешний объект. fn4 переприсваивает сам внешний x (параметр его не затеняет), поэтому x становится примитивом 5.
Типичные ошибки
- ✗Думать, что переприсваивание параметра (
x = 5) меняет переменную вызывающего, как мутация свойства - ✗Называть передачу объектов в JavaScript чистой передачей по ссылке — передаётся ссылка по значению
- ✗Считать, что параметр с именем как у внешней переменной делит с ней одну привязку
Уточняющие вопросы
- →Почему мутация свойства видна снаружи функции, а переприсваивание параметра — нет?
- →Как
fn4достаёт до внешнегоx, аfn3не может?
JuniorКодЧастоВернуть уникальные значения массива
Вернуть уникальные значения массива
Передать массив в Set, который отбрасывает дубликаты, сохраняя порядок вставки, затем развернуть обратно: [...new Set(arr)]. Проверка принадлежности у Set использует SameValueZero, поэтому примитивы дедуплицируются корректно (и NaN считается равным самому себе). Это O(n) и никогда не изменяет вход.
Типичные ошибки
- ✗Сортировать для дедупликации, разрушая исходный порядок первого появления
- ✗Считать, что
Setдедуплицирует объекты по значению — он делает это по ссылке - ✗Использовать
indexOfвнутри цикла, превращая задачу O(n) в O(n²)
Уточняющие вопросы
- →Чем проверка принадлежности у
SetобрабатываетNaNиначе, чем===? - →Как дедуплицировать массив объектов по конкретному свойству?
SeniorКодЧастоРеализовать curry(fn)
Реализовать curry(fn)
Вернуть рекурсивную curried(...args): если args.length >= fn.length, вызвать fn(...args); иначе вернуть новую функцию, принимающую ещё аргументы и вызывающую curried(...args, ...next), накапливая их в замыкании. fn.length (объявленная арность) — это порог, поэтому все формы частичного применения собирают аргументы, пока их не наберётся достаточно.
Типичные ошибки
- ✗Жёстко задавать арность вместо чтения
fn.length - ✗Считать, что ровно один аргумент на вызов, ломая форму
(1, 2)(3) - ✗Делить один изменяемый массив аргументов между ветками, из-за чего частичные применения мешают друг другу
Уточняющие вопросы
- →Почему
fn.length— верный порог, и когда он сообщает неверную арность? - →Как поддержать аргументы-заполнители, чтобы пропустить позицию параметра?
SeniorКодЧастоГлубоко слить два простых объекта
Глубоко слить два простых объекта
Начать с поверхностной копии { ...a }. Для каждого ключа b: если и a[key], и b[key] — простые объекты, присвоить ключу результат рекурсивного deepMerge(a[key], b[key]); иначе присвоить b[key]. Рекурсия сливает вложенные объекты, а копирование сначала означает, что входы не изменяются. Не-объектные значения из b перезаписывают.
Типичные ошибки
- ✗Использовать
Object.assignили спред, которые сливают только верхний уровень (поверхностно) - ✗Изменять
aна месте вместо построения и возврата нового объекта - ✗Считать массивы или
nullпростыми объектами для рекурсии
Уточняющие вопросы
- →Как решить, является ли значение «простым объектом», а не массивом или
null? - →Как слияние должно вести себя, когда один ключ хранит массив с каждой стороны?
SeniorКодЧастоЧто выведет этот фрагмент на привязку this и почему?
Что выведет этот фрагмент на привязку this и почему?
Печатает obj, затем '' три раза. this задаётся местом вызова, а не местом определения функции. obj.method() привязывает this к obj. Оторванный fn() и колбэк setTimeout — обычные вызовы, поэтому this — глобальный объект (window.name это ''). У стрелки нет своего this; она наследует внешний this модуля, тоже давая ''.
Типичные ошибки
- ✗Думать, что
thisфиксируется при определении, а не задаётся местом вызова - ✗Считать, что стрелка внутри литерала объекта привязывает
thisк этому объекту - ✗Ожидать, что оторванный метод сохраняет исходного получателя
Уточняющие вопросы
- →Как оторванный
fn()повёл бы себя иначе в строгом режиме? - →Как передача
obj.method.bind(obj)вsetTimeoutизменит вывод?
SeniorКодЧастоРеализовать throttle(fn, limit)
Реализовать throttle(fn, limit)
Хранить в замыкании lastRun (метку времени), стартующую так, чтобы первый вызов прошёл. На каждом вызове вычислять now = Date.now(); если now - lastRun >= limit, установить lastRun = now и запустить fn.apply(this, args), иначе игнорировать вызов. Это ограничивает fn одним запуском за окно limit по переднему фронту, сохраняя this и аргументы.
Типичные ошибки
- ✗Путать throttle с debounce — throttle ограничивает частоту, debounce ждёт паузу
- ✗Инициализировать
lastRunтак, что самый первый вызов ошибочно пропускается - ✗Терять
this/аргументы, вызываяfn()напрямую вместоfn.apply(this, args)
Уточняющие вопросы
- →Как добавить завершающий вызов, чтобы последнее обращение не терялось?
- →Почему версия с передним фронтом срабатывает на первом вызове сразу?
JuniorКодИногдаСделать методы объекта цепочечными, чтобы вызовы шли единой последовательностью
Сделать методы объекта цепочечными, чтобы вызовы шли единой последовательностью
Каждый изменяющий метод обновляет состояние экземпляра и затем делает return this. Возврат того же объекта позволяет следующему вызову в цепочке работать с ним, поэтому append('a').append('b').upper() читается как единая последовательность. Завершающий геттер value возвращает сам результат, так как завершает цепочку — ровно так делает текучие API jQuery.
Типичные ошибки
- ✗Возвращать изменённое поле (
this.str) вместоthis, что ломает следующий вызов метода - ✗Ничего не возвращать (
undefined), из-за чего второй метод цепочки бросает ошибку - ✗Мутировать в
append, но забывать, чтоupper()тоже должен делатьreturn this, чтобы оставаться цепочечным
Уточняющие вопросы
- →Почему цепочечный метод должен возвращать
this, а не обновлённое значение? - →Чем нецепочечный завершающий метод вроде
valueотличается от цепочечных?
JuniorКодИногдаВычислить факториал рекурсивно
Вычислить факториал рекурсивно
Базовый случай: когда n равно 0 (или 1), вернуть 1. Рекурсивный случай: вернуть n * factorial(n - 1). Каждый вызов умножает n на факториал n - 1, пока базовый случай не остановит рекурсию. Именно базовый случай на 0 обеспечивает, что factorial(0) корректно даёт 1.
Типичные ошибки
- ✗Пропускать базовый случай, вызывая бесконечную рекурсию и переполнение стека
- ✗Рекурсировать к
n + 1вместоn - 1, из-за чего она не завершается - ✗Возвращать
0в базовом случае, что обнуляет всё произведение
Уточняющие вопросы
- →Почему отсутствие базового случая вызывает переполнение стека
RangeError? - →Как переписать это итеративно, чтобы избежать глубокой рекурсии?
JuniorКодИногдаЗагрузить картинку по клику и показать плейсхолдер, успех и ошибку
Загрузить картинку по клику и показать плейсхолдер, успех и ошибку
Очистить контейнер (replaceChildren()), создать img, задать серый фон-плейсхолдер и навесить обработчики ДО установки src: img.onload показывает картинку, img.onerror ставит красный фон. Установка src последней запускает загрузку, и срабатывает ровно одно из двух событий. Очистка вначале означает, что повторный клик заменяет прошлый результат.
Типичные ошибки
- ✗Задавать
srcдо навешиванияonload/onerror, рискуя пропустить событие у закэшированной картинки - ✗Ожидать, что загрузка картинки синхронна (читать
completeсразу послеsrc), а не событийна - ✗Забывать сначала очистить контейнер, из-за чего клики копят картинки вместо замены
Уточняющие вопросы
- →Почему навешивать
onload/onerrorдо присваиванияsrc? - →Какое событие срабатывает при битом URL картинки, а какое при успехе?
JuniorКодИногдаРазвернуть строку за O(n)
Развернуть строку за O(n)
Разбить на символы, развернуть и склеить: s.split(''), затем .reverse() и .join(''), либо [...s].reverse().join(''). Форма со спредом лучше обрабатывает многобайтовые кодовые точки, чем split(''). Ручной цикл, собирающий результат с последнего индекса вниз, тоже работает за O(n) и возвращает '' для пустой строки.
Типичные ошибки
- ✗Вызывать
reverseпрямо на строке —reverseесть только у массивов - ✗Использовать
split(''), полагая, что он корректно обрабатывает суррогатные пары и эмодзи - ✗Забывать, что строки неизменяемы, поэтому надо вернуть новую строку
Уточняющие вопросы
- →Почему
[...s]разбивает суррогатные пары корректнее, чемs.split('')? - →Как развернуть порядок слов в предложении, но не буквы внутри них?
MiddleКодИногдаПостроить цепочечный калькулятор с приватным значением только для чтения
Построить цепочечный калькулятор с приватным значением только для чтения
Хранить накопленную сумму в переменной замыкания. Вернуть объект, чьи add/div меняют её и делают return this для цепочки. Открыть сумму только через аксессор get value() — без сеттера calc.value = 100 молча игнорируется, поэтому состояние остаётся приватным. В div проверить делитель и сделать throw до деления, чтобы деление на ноль не тронуло сумму.
Типичные ошибки
- ✗Открывать сумму как записываемое свойство вместо геттера, из-за чего внешний код может её испортить
- ✗Делить до проверки на ноль, из-за чего
div(0)меняет значение (наInfinity/NaN) до исключения - ✗Возвращать число из
add/divвместоthis, ломая цепочку
Уточняющие вопросы
- →Как
valueтолько с геттером сохраняет сумму приватной по сравнению с публичным полем? - →Как приватные поля
#класса дали бы ту же инкапсуляцию, что и переменная замыкания?
MiddleКодИногдаПостроить цепочечную обёртку $ в стиле jQuery над несколькими DOM-узлами
Построить цепочечную обёртку $ в стиле jQuery над несколькими DOM-узлами
Сохранить результат querySelectorAll на экземпляре. Каждый метод проходит по this.elements, применяя изменение — classList.add/remove, Object.entries(obj) в el.style, el.innerHTML = s — затем делает return this, чтобы обёртка цеплялась. Методы живут один раз на прототипе класса, а не пересоздаются на каждый элемент, и один вызов разводит операцию по всему найденному набору.
Типичные ошибки
- ✗Использовать
querySelector(один узел), когда API должен работать со всеми найденными элементами - ✗Забывать
return this, из-за чего у второго метода цепочки нет обёртки для вызова - ✗Пересоздавать обёртку или методы на каждый элемент вместо однократного определения в классе
Уточняющие вопросы
- →Почему обёртка-класс эффективнее возврата свежего объекта методов на каждый вызов
$()? - →Как
return thisнад списком узлов позволяет одним вызовом развести операцию по многим элементам?
MiddleКодИногдаНайти число, встречающееся один раз, когда все остальные встречаются дважды
Найти число, встречающееся один раз, когда все остальные встречаются дважды
Сложите все элементы по XOR — nums.reduce((a, b) => a ^ b, 0). XOR коммутативен и ассоциативен, x ^ x === 0, а x ^ 0 === x, поэтому каждая пара сокращается до нуля и выживает только одиночное значение. Это O(n) по времени и O(1) по памяти, что превосходит базовый хеш-подсчёт, требующий O(n) памяти.
Типичные ошибки
- ✗Хвататься за хеш-таблицу или подсчёт через
Set, что решает задачу, но за O(n) памяти - ✗Забывать, что нейтральный элемент XOR —
0, поэтому reduce должен стартовать с0 - ✗Считать, что XOR работает только на отсортированном входе — порядок ему не важен
Уточняющие вопросы
- →Как изменится подход с XOR, если каждый прочий элемент встречался бы трижды?
- →Почему старт
reduceс0, а не сnums[0], сохраняет код корректным?
SeniorКодИногдаПроверить строку на палиндром за O(n) времени, O(1) памяти
Проверить строку на палиндром за O(n) времени, O(1) памяти
Два указателя l = 0 и r = s.length - 1 идут к центру. Пропускайте сторону, чей символ не проходит проверку на букву (например, регуляркой вроде /[a-z]/i), затем сравнивайте через toLowerCase; несовпадение возвращает false. Встреча в центре возвращает true. Это O(n) по времени и O(1) дополнительной памяти — очищенная копия не создаётся.
Типичные ошибки
- ✗Строить очищенную или развёрнутую копию, что стоит O(n) памяти вместо O(1)
- ✗Сравнивать символы без приведения обеих сторон к нижнему регистру
- ✗Забыть пропускать не-буквы с ОБЕИХ сторон перед каждым сравнением
Уточняющие вопросы
- →Как заодно учитывать цифры как значимые символы?
- →Почему пропуск не-букв внутри цикла сохраняет общую сложность O(n)?