Собеседование на должность JavaScript-разработчика: популярные задачи с разбором решений

Обложка поста

Перевод статьи «How to Beat 5 Common JavaScript Interview Challenges»

О том, как проводить собеседования на технические специальности, уже давно ведётся множество споров. Не все компании используют одинаковые методы проверки кандидатов, но чаще всего вам предлагают решить задачу с помощью кода либо на подходящей платформе, либо на ужасной белой доске.

Проблема распространённых задач заключается в том, что они не имеют никакого отношения к реальной работе, особенно когда речь идёт о frontend. На собеседовании не хватает простых вопросов, например о совместимости браузера, вёрстке или событиях DOM.

Специалисты по подбору сотрудников, которые поддерживают такой подход, часто говорят что-то вроде:

— Я бы предпочёл нанять умного человека и научить его Х, чем нанять человека, который знает всё об Х, но ему не хватает творчества, логики и умения рассуждать.

Собеседование на должность frontend-инженера в Сан-Франциско

Как бы то ни было, на данный момент задачи всё ещё являются важной частью процесса собеседования. Эта статья расскажет, как решить пять распространённых задач по программированию, которые вам могут задать в ходе собеседования на должность младшего JavaScript- или frontend-разработчика.

Подготовка к собеседованию

Прежде чем приступить к решению задач, рассмотрим несколько общих советов по подготовке к собеседованию.

  • Готовьтесь заранее. Определите приоритет в подготовке, изучайте менее знакомые темы и много практикуйтесь. Если у вас нет опыта работы в области компьютерных наук, обязательно ознакомьтесь с некоторыми фундаментальными темами, касающимися алгоритмов и структур данных. Существуют онлайн-платформы, как платные, так и бесплатные, которые предлагают отличные способы отработать свои навыки прохождения собеседований. GeeksforGeeks, Pramp, Interviewing.io и CodeSignal — лишь некоторые из них.
  • Тренируйтесь мыслить вслух, когда пытаетесь найти решение. Фактически, раскрыть ваш ход мыслей в обстановке собеседования предпочтительнее, чем тратить всё доступное время на написание кода в полной тишине. Ваши слова дадут интервьюеру шанс помочь, если вы ошибётесь. Это также подчеркнёт ваши навыки общения.
  • Осознайте задачу, прежде чем начать её решать. Это важно. Иначе вы потратите время на размышления о неверной задаче. Кроме того, это заставит задуматься над вопросами, которые вы можете задать интервьюеру.
  • Практикуйте написание кода вручную. Это поможет, если вам предложат использовать доску, где нет автозаполнения, форматирования и т. д. При подготовке пробуйте записать свой код на листе бумаги или доске вместо того, чтобы держать всё в голове.

Стандартные задачи на JavaScript

Вполне вероятно, что вы уже сталкивались с одной или несколькими задачами из перечисленных ниже либо во время собеседования, либо во время практики на JavaScript. Что может быть лучше, чем наконец разобраться с ними?

Палиндром

Палиндром — это слово, предложение или последовательность символов, которая читается слева направо так же, как и справа налево. Например, «racecar» и «Anna» являются палиндромами, а «Table» и «John» — нет.

Формулировка задачи

Дана строка, нужно написать функцию, которая возвращает значение true, если строка — палиндром, и false, если это не так. При решении задачи необходимо учитывать пробелы и знаки препинания. Пример:

palindrome('racecar')  ===  true
palindrome('table')  ===  false

Логическое решение задачи

Эта задача строится на идее перевернуть строку задом наперёд. Если обратная строка совпадает с исходной, то это палиндром, и функция должна вернуть значение true. И наоборот, если обратная строка не совпадает с исходной, функция должна вернуть значение false.

Практическое решение

Вот один из способов решения задачи палиндрома:

const palindrome = str => {
  // Меняем регистр строки на нижний
  str = str.toLowerCase()
  // Переворачиваем строку и возвращаем результат сравнения
  return str === str.split('').reverse().join('')
}

Начните с преобразования символов входной строки в символы нижнего регистра. Поскольку вы собираетесь сравнивать каждый символ исходной строки с символами перевёрнутой строки, единый регистр гарантирует, что сравнение будет сосредоточено лишь на сравнении самих символов.

Далее переворачиваем исходную строку. Это можно сделать, преобразовав строку в массив с помощью метода .split() (библиотека String), затем перевернув массив методом .reverse() (библиотека Array) и, наконец, преобразовав обратный массив снова в строку с помощью метода .join() (библиотека Array).

Остаётся сравнить перевёрнутую строку с исходной и вернуть результат в виде true или false.

FizzBuzz

Это очень популярная задача на собеседованиях.

Формулировка задачи

Напишите функцию, которая выводит в консоль числа от 1 до n, где n — целое число, которое функция принимает в качестве параметра, при этом:

  • выводит fizz вместо чисел, кратных 3;
  • выводит buzz вместо чисел, кратных 5;
  • выводит fizzbuzz вместо чисел, кратных и 3, и 5.

Пример:

fizzBuzz(5)

Результат:

// 1
// 2
// fizz
// 4
// buzz

Логическое решение задачи

Один важный момент в FizzBuzz — способ поиска кратных чисел в JavaScript. Это делается с помощью оператора модуля или остатка — %, который возвращает остаток от деления двух чисел. Остаток 0 означает, что первое число кратно второму:

12 % 5 // 2 -> 12 не кратно 5
12 % 3 // 0 -> 12 кратно 3

Если разделить 12 на 5, то получится 2 с остатком 2. Если разделить 12 на 3, то получится 4 с остатком 0. В первом примере 12 не кратно 5, а в во втором 12 кратно 3.

Практическое решение

Вот одно из решений, которое вы можете использовать для задачи FizzBuzz:

const fizzBuzz = num => {
  for(let i = 1; i <= num; i++) {
    // Проверяем, кратно ли число 3 и 5
    if(i % 3 === 0 && i % 5 === 0) {
      console.log('fizzbuzz')
    } // Проверяем, кратно ли число 3
      else if(i % 3 === 0) {
      console.log('fizz')
    } // Проверяем, кратно ли число 5
      else if(i % 5 === 0) {
      console.log('buzz')
    } else {
      console.log(i)
    }
  }
}

Функция выше попросту выполняет необходимые тесты, используя условные операторы, и выводит ожидаемый результат. В этой задаче нужно обратить внимание на порядок операторов if… else: начинайте с двойного условия (&&) и заканчивайте случаем, когда кратные числа не найдены. Таким образом вы сможете охватить все варианты.

Анаграмма

Слова являются анаграммами другого слова, если имеют одинаковые буквы в одинаковом количестве, но в разном порядке.

Формулировка задачи

Напишите функцию, проверяющую, являются ли две строки анаграммами друг друга (регистр букв не имеет значения). Важны только символы, пробелы или знаки препинания не учитываются. Пример:

anagram('finder', 'Friend')  --> true
anagram('hello', 'bye') --> false

Логическое решение задачи

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

finder --> f: 1        friend --> f: 1
           i: 1                   r: 1
           n: 1                   i: 1
           d: 1                   e: 1
           e: 1                   n: 1
           r: 1                   d: 1

Подходящей структурой для хранения данных анаграммы будет объектный литерал JavaScript: ключ — это символ буквы, а значение — количество её повторений в данной строке.

Затем обратите внимание на пограничные случаи задачи:

  • убедитесь, что регистр букв не учитывается при сравнении. Просто преобразуйте обе строки в нижний или верхний регистр;
  • исключите из сравнения всё, что не является символом. Хорошим вариантом будет использование регулярных выражений.

Практическое решение

Вот как можно подойти к решению этой задачи:

// Вспомогательная функция, которая создаёт объект для хранения данных
const buildCharObject = str => {
  const charObj = {}
  for(let char of str.replace(/[^\w]/g).toLowerCase()) {
    // Если объект уже содержит пару ключ-значение равную значению в цикле,
    // увеличиваем значение на 1, в противном случае добавляем букву в качестве ключа
    // и 1 в качестве значения
    charObj[char] = charObj[char] + 1 || 1
  }
  return charObj
}
// Главная функция
const anagram = (strA, strB) => {
  // Создаём объект для хранения strA
  const aCharObject = buildCharObject(strA)
  // Создаём объект для хранения strB
  const bCharObject = buildCharObject(strB)
  // Сравниваем количество ключей в обоих объектах 
  // (анаграммы должны иметь одинаковое количество букв)
  if(Object.keys(aCharObject).length !== Object.keys(bCharObject).length) {
    return false
  }
  // Если оба объекта имеют одинаковое количество ключей, мы можем быть уверены,
  // что обе строки имеют одинаковое количество символов. Теперь мы можем сравнить
  // оба объекта, чтобы увидеть, имеют ли они одинаковые буквы в одинаковом количестве
  for(let char in aCharObject) {
    if(aCharObject[char] !== bCharObject[char]) {
      return false
    }
  }
  // Если проверка успешна, строки являются анаграммами — возвращаем true
  return true
}

Обратите внимание на использование Object.keys(). Этот метод возвращает массив, содержащий имена или ключи в том же порядке, в котором они встречаются в объекте. В этом случае массив будет выглядеть так:

['f', 'i', 'n', 'd', 'e', 'r']

Это удобный способ получить свойства объекта без выполнения громоздкого цикла. В этой задаче вы можете использовать этот способ вместе со свойством .length, чтобы проверить, имеют ли обе строки одинаковое количество символов, что является важной особенностью анаграмм.

Найти гласные

Вероятно, это одна из менее сложных задач, но это не уменьшает шанс того, что вы столкнётесь с ней во время собеседования.

Формулировка задачи

Напишите функцию, которая принимает строку в качестве аргумента и возвращает количество гласных, содержащихся в этой строке. Гласными являются «a», «e», «i», «o», «u».

Примеры:

findVowels('hello') // --> 2
findVowels('why') // --> 0

Практическое решение

Вот простое решение задачи с гласными:

const findVowels = str => {
  let count = 0
  const vowels = ['a', 'e', 'i', 'o', 'u']
  for(let char of str.toLowerCase()) {
    if(vowels.includes(char)) {
      count++
    }
  }
  return count
}

На что следует обратить внимание — это использование метода .includes(). Он доступен как для строк, так и для массивов. Вы можете использовать его, чтобы определить, содержит ли массив определённое значение. Метод возвращает true, если массив содержит указанное значение, и false, если нет.

Существует также более краткое решение этой проблемы:

const findVowels = str => {
  const matches = str.match(/[aeiou]/gi)
  return matches ? matches.length : 0
}

Решение выше задействует метод .match(). Его использование в сочетании с регулярным выражением позволяет выполнять действенный поиск. Если регулярное выражение как аргумент метода найдено внутри указанной строки, возвращаемым значением будет массив совпадающих символов. Если совпадений не найдено, .match() вернёт null.

Фибоначчи

Эта статья не была бы полной без задачи Фибоначчи — классического вопроса, с которым вы наверняка столкнётесь во время собеседования.

Последовательность Фибоначчи — это порядок чисел, где каждое последующее число является суммой двух предыдущих. Например, первые десять чисел последовательности выглядят так: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34.

Формулировка задачи

Напишите функцию, которая возвращает n-ую запись в последовательности, где n — это число, которое вы передаёте в качестве аргумента функции.

Пример:

fibonacci(3)  // --> 2

Логическое решение задачи

Задача Фибоначчи включает в себя прохождение по циклу такое количество раз, которое указано в аргументе функции, и возврат значения на соответствующей позиции. Такой способ постановки задачи указывает на использование циклов для решения. В качестве альтернативы вы можете попробовать рекурсию, что наверняка поразит интервьюера.

Практическое решение

Вот как может выглядеть решение с помощью цикла:

const fibonacci = num => {
  // Сохраняем последовательность Фибоначчи, которую собираемся сгенерировать,
  // внутри массива и инициализируем массив первыми двумя числами последовательности
  const result = [0, 1]

  for(let i = 2; i <= num; i++) {
    // Поместим сумму двух чисел, предшествующих позиции i в массиве результатов, 
    // в конец этого массива
    const prevNum1 = result[i - 1]
    const prevNum2 = result[i - 2]
    result.push(prevNum1 + prevNum2)
  }
  // Вернём последнее значение из массива результатов
  return result[num]
}

Массив результатов уже содержит первые два числа в своём ряду, потому что каждая запись в ряду Фибоначчи состоит из суммы двух предыдущих чисел. Изначально у вас нет двух чисел, которые вы можете взять для получения следующего числа, поэтому цикл не может сгенерировать их автоматически. Однако вы знаете, что первые два числа всегда 0 и 1, поэтому вручную инициализируете массив результатов этими двумя числами.

Теперь попробуем рекурсивный подход:

const fibonacci = num => {
  // Если num равно 0 или 1, возвращаем num
  if(num < 2) {
    return num
  }
  // Рекурсия здесь
  return fibonacci(num - 1) + fibonacci(num - 2)
}

Вы продолжаете вызывать fibonacci(), передавая всё меньшие и меньшие числа в качестве аргументов, пока не достигнете уже известного случая, где переданный аргумент равен 0 или 1.

Заключение

Если вы уже прошли несколько этапов собеседований на работу frontend- или JavaScript-разработчика, особенно уровня junior, вы, вероятно, столкнулись хотя бы с одной или двумя из задач, которые перечислены выше. Но даже если они вам не встретились, вы можете использовать их для совершенствования своих навыков программирования на JavaScript.

Ещё больше IT-задач для собеседований с разбором решений можно найти здесь.

Не смешно? А здесь смешно: @ithumor