← Курс/Типичные задачи на JS собеседовании#139 из 257+40 XP

Типичные задачи на JS собеседовании

Краткий ответ

На JS-собеседованиях чаще всего просят реализовать: deepClone (рекурсивное копирование), flattenArray (разворачивание вложенных массивов), groupBy (группировка объектов), pipe (цепочка функций), once (однократный вызов), reverse/palindrome, поиск дубликатов. Важно знать сложность каждого решения и уметь обсуждать trade-offs.

Полный разбор

1. deepClone — глубокое копирование

// Наивные подходы и их проблемы:
const copy1 = { ...obj }         // поверхностная копия (shallow)
const copy2 = JSON.parse(JSON.stringify(obj))  // не копирует Date, undefined, функции

// Правильная реализация:
function deepClone(value) {
  // null и примитивы — возвращаем как есть
  if (value === null || typeof value !== 'object') return value

  // Date — создаём новый объект
  if (value instanceof Date) return new Date(value.getTime())

  // Array — рекурсивно копируем каждый элемент
  if (Array.isArray(value)) {
    return value.map(item => deepClone(item))
  }

  // Object — рекурсивно копируем каждое свойство
  const cloned = {}
  for (const key in value) {
    if (Object.prototype.hasOwnProperty.call(value, key)) {
      cloned[key] = deepClone(value[key])
    }
  }
  return cloned
}
// O(n) время и пространство, где n — количество узлов в структуре

2. flattenArray — разворачивание массивов

// Встроенный: arr.flat(Infinity) — но может быть запрещён
function flattenArray(arr, depth = Infinity) {
  if (depth === 0) return [...arr]

  return arr.reduce((acc, item) => {
    if (Array.isArray(item) && depth > 0) {
      acc.push(...flattenArray(item, depth - 1))
    } else {
      acc.push(item)
    }
    return acc
  }, [])
}

flattenArray([1, [2, [3, [4]]]])           // [1, 2, 3, 4]
flattenArray([1, [2, [3, [4]]]], 1)        // [1, 2, [3, [4]]]
// O(n) где n — общее количество элементов

3. groupBy — группировка по свойству

function groupBy(array, key) {
  return array.reduce((groups, item) => {
    const groupKey = typeof key === 'function' ? key(item) : item[key]
    ;(groups[groupKey] ??= []).push(item)
    return groups
  }, {})
}

const orders = [
  { id: 1, status: 'done', amount: 100 },
  { id: 2, status: 'pending', amount: 200 },
  { id: 3, status: 'done', amount: 50 },
]

groupBy(orders, 'status')
// { done: [{id:1,...}, {id:3,...}], pending: [{id:2,...}] }

groupBy(orders, o => o.amount > 100 ? 'big' : 'small')
// { small: [{id:1,...}, {id:3,...}], big: [{id:2,...}] }
// O(n) — один проход

4. pipe — последовательная композиция

function pipe(...fns) {
  return (x) => fns.reduce((acc, fn) => fn(acc), x)
}

const process = pipe(
  x => x * 2,
  x => x + 1,
  x => x.toString()
)
process(5)  // '11'  (5*2=10, 10+1=11, String(11)='11')
// O(n) где n — количество функций

5. once — однократный вызов

function once(fn) {
  let called = false
  let result

  return function(...args) {
    if (!called) {
      called = true
      result = fn.apply(this, args)
    }
    return result  // всегда возвращаем тот же результат
  }
}

const initialize = once(() => {
  console.log('Инициализация (один раз)')
  return { ready: true }
})

initialize()  // выполняется
initialize()  // не выполняется, возвращает тот же результат
initialize()  // не выполняется
// O(1) после первого вызова

6. Реверс строки и палиндром

// Реверс строки
function reverseString(str) {
  return str.split('').reverse().join('')
  // или: return [...str].reverse().join('')  (корректно для Unicode)
}

// Проверка палиндрома
function isPalindrome(str) {
  // Нормализуем: убираем не-буквы, приводим к нижнему регистру
  const clean = str.toLowerCase().replace(/[^a-zа-яё]/g, '')
  return clean === clean.split('').reverse().join('')
}

isPalindrome('racecar')     // true
isPalindrome('A man a plan a canal Panama')  // true
isPalindrome('hello')       // false
// O(n) время, O(n) пространство

7. Поиск дубликатов в массиве

// Вариант 1: через Set — O(n) время и пространство
function findDuplicates(arr) {
  const seen = new Set()
  const duplicates = new Set()

  for (const item of arr) {
    if (seen.has(item)) {
      duplicates.add(item)
    } else {
      seen.add(item)
    }
  }

  return [...duplicates]
}

// Вариант 2: через reduce + Map (с подсчётом)
function findDuplicatesWithCount(arr) {
  const counts = arr.reduce((map, item) => {
    map.set(item, (map.get(item) || 0) + 1)
    return map
  }, new Map())

  return [...counts.entries()]
    .filter(([_, count]) => count > 1)
    .map(([item, count]) => ({ item, count }))
}

findDuplicates([1, 2, 3, 2, 4, 3, 5])  // [2, 3]
findDuplicatesWithCount([1, 2, 2, 3, 3, 3])
// [{ item: 2, count: 2 }, { item: 3, count: 3 }]

Сложность алгоритмов — шпаргалка

| Функция | Время | Память |

|---------|-------|--------|

| deepClone | O(n) | O(n) |

| flattenArray | O(n) | O(n) |

| groupBy | O(n) | O(n) |

| pipe | O(k) | O(1) |

| once | O(1) | O(1) |

| reverseString | O(n) | O(n) |

| findDuplicates (Set) | O(n) | O(n) |

Связанные уроки курса

  • Функции — замыкания в once, pipe, compose
  • Массивы — flattenArray, findDuplicates, map/filter/reduce
  • Объекты — groupBy, deepClone объектов
  • Копирование объектов — shallow vs deep clone, structuredClone
  • Рекурсия — deepClone и flattenArray используют рекурсию
  • Замыкания — once, pipe, memoize реализованы через замыкания
  • Как отвечать на собеседовании

    Перед написанием кода проговори вслух: «Я понимаю задачу так...», уточни edge cases (null, вложенные массивы, циклические ссылки). Напиши решение, затем скажи сложность. Предложи оптимизацию если видишь её. Для deepClone обязательно обсуди: что делать с Date, Map, Set, функциями, циклическими ссылками — интервьюер оценит осознанность.

    Красные флаги ответа

  • JSON.parse(JSON.stringify(obj)) как «глубокое копирование» без оговорок — теряет Date, undefined, функции, Symbol, Map, Set — нельзя называть это полным решением
  • Незнание временной сложности своего решения — «я не думал об этом» — плохой сигнал для middle+ позиции
  • Решение без обработки edge cases — deepClone без проверки на null, flattenArray без проверки на непустой массив — код упадёт в продакшне
  • Примеры

    Все 7 функций с реализацией и тестами: deepClone, flattenArray, groupBy, pipe, once, reverse/palindrome, findDuplicates

    // ===== 1. DEEP CLONE =====
    console.log('=== 1. deepClone ===')
    
    function deepClone(value) {
      if (value === null || typeof value !== 'object') return value
      if (value instanceof Date) return new Date(value.getTime())
      if (Array.isArray(value)) return value.map(item => deepClone(item))
    
      const cloned = {}
      for (const key in value) {
        if (Object.prototype.hasOwnProperty.call(value, key)) {
          cloned[key] = deepClone(value[key])
        }
      }
      return cloned
    }
    
    const original = {
      name: 'Алиса',
      scores: [10, 20, 30],
      address: { city: 'Москва', zip: '101000' },
      birthday: new Date('1990-01-15')
    }
    
    const cloned = deepClone(original)
    cloned.scores.push(40)
    cloned.address.city = 'Питер'
    
    console.log('original.scores:', original.scores)       // [10, 20, 30] — не изменился
    console.log('cloned.scores:', cloned.scores)           // [10, 20, 30, 40]
    console.log('original.address:', original.address.city) // 'Москва'
    console.log('cloned.address:', cloned.address.city)     // 'Питер'
    console.log('Date сохранён:', cloned.birthday instanceof Date, cloned.birthday.getFullYear())
    
    // ===== 2. FLATTEN ARRAY =====
    console.log('\n=== 2. flattenArray ===')
    
    function flattenArray(arr, depth = Infinity) {
      return arr.reduce((acc, item) => {
        if (Array.isArray(item) && depth > 0) {
          acc.push(...flattenArray(item, depth - 1))
        } else {
          acc.push(item)
        }
        return acc
      }, [])
    }
    
    console.log(flattenArray([1, [2, 3], [4, [5, 6]]]))           // [1, 2, 3, 4, 5, 6]
    console.log(flattenArray([1, [2, [3, [4, [5]]]]]))             // [1, 2, 3, 4, 5]
    console.log(flattenArray([1, [2, [3, [4]]]], 1))               // [1, 2, [3, [4]]]
    console.log(flattenArray([1, [2, [3, [4]]]], 2))               // [1, 2, 3, [4]]
    console.log(flattenArray([[1, 2], [3, [4, 5]], [[6]]]))        // [1, 2, 3, 4, 5, 6]
    
    // ===== 3. GROUP BY =====
    console.log('\n=== 3. groupBy ===')
    
    function groupBy(array, key) {
      return array.reduce((groups, item) => {
        const groupKey = typeof key === 'function' ? key(item) : item[key]
        ;(groups[groupKey] ??= []).push(item)
        return groups
      }, {})
    }
    
    const products = [
      { name: 'iPhone', category: 'phone', price: 999 },
      { name: 'Galaxy', category: 'phone', price: 799 },
      { name: 'MacBook', category: 'laptop', price: 1299 },
      { name: 'iPad', category: 'tablet', price: 599 },
      { name: 'ThinkPad', category: 'laptop', price: 899 },
    ]
    
    const byCategory = groupBy(products, 'category')
    console.log('По категориям:', Object.keys(byCategory))  // ['phone', 'laptop', 'tablet']
    console.log('Телефонов:', byCategory.phone.length)       // 2
    
    const byPrice = groupBy(products, p => p.price > 800 ? 'expensive' : 'affordable')
    console.log('Дорогих:', byPrice.expensive?.length)   // 3
    console.log('Доступных:', byPrice.affordable?.length) // 2
    
    // ===== 4. PIPE =====
    console.log('\n=== 4. pipe ===')
    
    function pipe(...fns) {
      return (x) => fns.reduce((acc, fn) => fn(acc), x)
    }
    
    const processOrder = pipe(
      order => ({ ...order, total: order.price * order.qty }),
      order => ({ ...order, tax: order.total * 0.2 }),
      order => ({ ...order, finalPrice: order.total + order.tax }),
      order => `Заказ #${order.id}: ${order.finalPrice.toFixed(2)}₽`
    )
    
    console.log(processOrder({ id: 101, price: 100, qty: 3 }))
    // 'Заказ #101: 360.00₽'  (300 + 60 tax)
    
    const transformText = pipe(
      s => s.trim(),
      s => s.toLowerCase(),
      s => s.replace(/\s+/g, '-'),
      s => s.replace(/[^a-z0-9-]/g, '')
    )
    
    console.log(transformText('  Hello World 2024!  '))  // 'hello-world-2024'
    
    // ===== 5. ONCE =====
    console.log('\n=== 5. once ===')
    
    function once(fn) {
      let called = false
      let result
    
      return function(...args) {
        if (!called) {
          called = true
          result = fn.apply(this, args)
        }
        return result
      }
    }
    
    let initCount = 0
    const initialize = once(() => {
      initCount++
      console.log('Инициализация выполнена!')
      return { sessionId: 'sess_' + Date.now(), ready: true }
    })
    
    const r1 = initialize()
    const r2 = initialize()
    const r3 = initialize()
    
    console.log('r1 === r2:', r1 === r2)      // true — тот же объект
    console.log('r1 === r3:', r1 === r3)      // true
    console.log('initCount:', initCount)       // 1 — выполнилось только раз
    console.log('ready:', r1.ready)            // true
    
    // ===== 6. REVERSE / PALINDROME =====
    console.log('\n=== 6. reverseString / isPalindrome ===')
    
    function reverseString(str) {
      return [...str].reverse().join('')  // [...str] корректен для Unicode
    }
    
    function isPalindrome(str) {
      const clean = str.toLowerCase().replace(/[^a-zа-яёa-z0-9]/g, '')
      return clean === [...clean].reverse().join('')
    }
    
    console.log(reverseString('hello'))        // 'olleh'
    console.log(reverseString('JavaScript'))   // 'tpircSavaJ'
    console.log(reverseString('12345'))        // '54321'
    
    console.log(isPalindrome('racecar'))       // true
    console.log(isPalindrome('level'))         // true
    console.log(isPalindrome('A man a plan a canal Panama'))  // true
    console.log(isPalindrome('hello'))         // false
    console.log(isPalindrome('Ротор'))         // true (русский)
    
    // ===== 7. FIND DUPLICATES =====
    console.log('\n=== 7. findDuplicates ===')
    
    function findDuplicates(arr) {
      const seen = new Set()
      const duplicates = new Set()
    
      for (const item of arr) {
        if (seen.has(item)) {
          duplicates.add(item)
        } else {
          seen.add(item)
        }
      }
    
      return [...duplicates]
    }
    
    function findDuplicatesWithCount(arr) {
      const counts = arr.reduce((map, item) => {
        map.set(item, (map.get(item) || 0) + 1)
        return map
      }, new Map())
    
      return [...counts.entries()]
        .filter(([_, count]) => count > 1)
        .map(([item, count]) => ({ item, count }))
    }
    
    console.log(findDuplicates([1, 2, 3, 2, 4, 3, 5]))    // [2, 3]
    console.log(findDuplicates(['a', 'b', 'a', 'c', 'b'])) // ['a', 'b']
    console.log(findDuplicates([1, 2, 3]))                  // [] — нет дубликатов
    
    console.log(findDuplicatesWithCount([1, 2, 2, 3, 3, 3]))
    // [{ item: 2, count: 2 }, { item: 3, count: 3 }]