Превратите свое собеседование с примерами общих и интересных вопросов

JavaScript считается отличным языком для начинающих. Отчасти это связано с тем, что он широко используется в Интернете, а отчасти потому, что некоторые его функции позволяют писать неидеальный код, который все еще работает: он не такой строгий, как многие другие языки, независимо от того, пропустили ли вы половину кода. -colon, или вы не хотите беспокоиться об управлении памятью.

Но к тому времени, когда вы будете готовы к собеседованию, вы захотите быть уверенными в том, что знаете все тонкости языка, в том числе некоторые вещи, которые выполняются автоматически и «за кулисами».

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

ЧАСТЬ I: ВОПРОСЫ ПО КРИВОЛЬНОМУ МЯЧУ

Было бы довольно сложно, если бы во время интервью возник какой-либо из следующих вопросов. Тем не менее, эти вопросы все равно должны оказаться полезными при подготовке: они раскрывают некоторые интересные особенности JavaScript и выделяют некоторые решения, которые необходимо принять при создании языка программирования в первую очередь.

Чтобы узнать о более необычных функциях JavaScript, я рекомендую посетить https://wtfjs.com.

1. Почему Math.max () меньше Math.min ()?

Тот факт, что Math.max() > Math.min() возвращает false, звучит неверно, но на самом деле в этом есть большой смысл.

Если аргументы не указаны, Math.min() возвращает infinity, а Math.max() возвращает -infinity. Это просто часть спецификации для методов max() и min(), но за выбором стоит хорошая логика. Чтобы понять почему, взгляните на следующий код:

Math.min(1) 
// 1
Math.min(1, infinity)
// 1
Math.min(1, -infinity)
// -infinity

Если -infinity считался аргументом по умолчанию для Math.min(), то каждый результат был бы -infinity, что бесполезно! Принимая во внимание, что если аргумент по умолчанию - infinity, добавление любого другого аргумента вернет это число - и это именно то поведение, которое мы хотим.

2. Почему 0,1 + 0,2 === 0,3 возвращает ложь?

Короче говоря, это связано с тем, насколько точно JavaScript может хранить двоичные числа с плавающей запятой. Если вы введете следующие уравнения в консоль Google Chrome, вы получите:

0.1 + 0.2
// 0.30000000000000004
0.1 + 0.2 - 0.2
// 0.10000000000000003
0.1 + 0.7
// 0.7999999999999999

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

Фиксированная точка

Например, если вам известна максимальная точность, которая вам понадобится (например, если вы имеете дело с валютами), вы можете использовать целочисленный тип для хранения значения. Поэтому вместо $4.99 вы можете сохранить 499 и выполнять для него любые уравнения. Затем вы можете отобразить результат для конечного пользователя, используя выражение типа result = (value / 100).toFixed(2), которое возвращает строку.

Десятичные дроби в двоичном коде

Если точность действительно важна, другой вариант - использовать формат двоично-десятичных знаков (BCD), к которому вы можете получить доступ в JavaScript с помощью этой библиотеки BCD. Каждое десятичное значение хранится отдельно в одном байте (8 бит). Это неэффективно, поскольку байт может хранить 16 отдельных значений, а эта система использует только значения 0–9. Однако, если для вашего приложения важна точность, возможно, стоит пойти на компромисс.

3. Почему 018 минус 017 равно 3?

Тот факт, что 018 — 017 возвращает 3, является результатом тихого преобразования типа. В данном случае речь идет о восьмеричных числах.

Краткое введение в восьмеричные числа

Вы, вероятно, знаете об использовании двоичной (основание-2) и шестнадцатеричной (основание-16) систем счисления в вычислениях, но восьмеричная (основа 8) также занимает видное место в истории компьютеров: в конце 1950-х и В 1960-х годах он использовался для сокращения двоичных файлов, что сокращало затраты на материалы в очень дорогих в производстве системах!

Вскоре после этого появилось шестнадцатеричное:

IBM 360 [выпущенная в 1965 году] сделала решительный шаг от восьмеричной системы к шестнадцатеричной. Те из нас, кто привык к восьмеричной системе, были потрясены ее экстравагантностью! - Воан Пратт

Восьмеричные числа сегодня

Но для чего восьмеричное число полезно в современных языках программирования? В некоторых случаях восьмеричный формат имеет преимущество перед шестнадцатеричным, так как не требует использования нечисловых цифр (вместо 0–7 используется число от 0 до 7).

Одно из распространенных применений - это права доступа к файлам для систем Unix, где существует ровно восемь вариантов разрешений:

   4 2 1
0  - - - no permissions
1  - - x only execute
2  - x - only write
3  - x x write and execute
4  x - - only read
5  x - x read and execute
6  x x - read and write
7  x x x read, write and execute

По тем же причинам он также используется для цифровых дисплеев.

Вернуться к вопросу

В JavaScript префикс 0 преобразует любое число в восьмеричное. Однако 8 не используется в восьмеричном формате, и любое число, содержащее 8, будет автоматически преобразовано в обычное десятичное число.

Следовательно, 018 — 017 фактически эквивалентно десятичному выражению 18 — 15, потому что 017 восьмеричное, а 018 десятичное.

ЧАСТЬ II: ОБЩИЕ ВОПРОСЫ

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

4. Чем выражение функции отличается от объявления функции?

В объявлении функции используется ключевое слово function, за которым следует имя функции. Напротив, выражение функции начинается с var, let или const, за которыми следует имя функции и оператор присваивания =. Вот некоторые примеры:

// Function Declaration
function sum(x, y) {
  return x + y;
};

// Function Expression: ES5
var sum = function(x, y) {
  return x + y;
};
// Function Expression: ES6+
const sum = (x, y) => { return x + y };

В использовании ключевое отличие состоит в том, что объявления функций поднимаются, а выражения функций - нет. Это означает, что объявления функций перемещаются интерпретатором JavaScript в верхнюю часть своей области, поэтому вы можете определить объявление функции и вызвать его в любом месте вашего кода. Напротив, вы можете вызывать выражение функции только в линейной последовательности: вы должны определить его, прежде чем вызывать.

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

  • Прежде всего, функциональные выражения обеспечивают более предсказуемую и структурированную кодовую базу. Конечно, структурированная кодовая база также возможна с объявлениями; просто объявления позволяют легче избавиться от запутанного кода.
  • Во-вторых, мы можем использовать синтаксис ES6 для функциональных выражений: он обычно более краток, а let и const обеспечивают больший контроль над тем, можно ли переназначить переменную или нет, как мы увидим в следующем вопросе.

5. В чем разница между var, let и const?

Я полагаю, что с момента выпуска ES6 это был довольно частый вопрос на собеседовании теми компаниями, которые в полной мере использовали более современный синтаксис. var было ключевым словом объявления переменной с самого первого выпуска JavaScript. Но его недостатки привели к принятию двух новых ключевых слов в ES6: let и const.

У этих трех ключевых слов разные подходы к назначению, поднятию и области действия, поэтому мы рассмотрим каждое из них отдельно.

i) Переуступка

Основное различие состоит в том, что let и var можно переназначить, а const нельзя. Это делает const лучший выбор для переменных, которые не нужно изменять, и предотвращает такие ошибки, как случайное повторное присвоение. Обратите внимание, что const допускает изменение переменных, что означает, что если он представляет массив или объект, они могут измениться. Вы просто не можете переназначить саму переменную.

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

ii) Подъем

Подобно разнице между объявлениями функций и выражениями (обсуждалось выше), переменные, объявленные с использованием var, всегда поднимаются в верхнюю часть своей соответствующей области, в то время как переменные, объявленные с использованием const и let, поднимаются, но если вы попытаетесь получить к ним доступ до того, как они объявлено, вы получите ошибку «временная мертвая зона». Это полезное поведение, поскольку var может быть более подвержено ошибкам, таким как случайное повторное назначение. Возьмем следующий пример:

var x = "global scope";
function foo() {
  var x = "functional scope";
  console.log(x);
}
foo(); // "functional scope"
console.log(x); // "global scope"

Здесь результат foo() и console.log(x) соответствует нашим ожиданиям. Но что, если мы бросим второй var?

var x = "global scope";
function foo() {
  x = "functional scope";
  console.log(x);
}
foo(); // "functional scope"
console.log(x); // "functional scope"

Несмотря на то, что он определен в функции, x = "functional scope" переопределил глобальную переменную. Нам нужно было повторить ключевое слово var, чтобы указать, что вторая переменная x имеет область видимости только до foo().

iii) Объем

В то время как var имеет функциональную область видимости, let и const имеют область видимости блока: как правило, блок - это любой код в фигурных скобках {}, включая функции, условные операторы и циклы. Чтобы проиллюстрировать разницу, взгляните на следующий код:

var a = 0; 
let b = 0;
const c = 0;
if (true) {
  var a = 1;
  let b = 1; 
  const c = 1;
}
console.log(a); // 1
console.log(b); // 0
console.log(c); // 0

В нашем условном блоке глобальный диапазон var a был переопределен, а глобальный let b и const c - нет. В целом, если локальные назначения остаются локальными, код будет более чистым и будет меньше ошибок.

6. Что произойдет, если вы назначите переменную без ключевого слова?

Что, если вы определяете переменную вообще без использования ключевого слова? Технически, если x еще не определен, x = 1 является сокращением для window.x = 1. Я обсуждал это в недавней статье об управлении памятью в JavaScript, поскольку это частая причина утечек памяти.

Чтобы полностью предотвратить это сокращение, вы можете использовать строгий режим, представленный в ES5, путем написания use strict в верхней части документа или конкретной функции. Затем, когда вы попытаетесь объявить переменную без ключевого слова, вы получите сообщение об ошибке: Uncaught SyntaxError: Unexpected indentifier.

7. В чем разница между объектно-ориентированным программированием (ООП) и функциональным программированием (ФП)?

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

Существует множество различных парадигм программирования, но в современных вычислениях два самых популярных стиля - это функциональное программирование (FP) и объектно-ориентированное программирование (ООП), и JavaScript может делать и то, и другое.

Объектно-ориентированное программирование

ООП основано на концепции «объектов». Это структуры данных, которые содержат поля данных, известные в JavaScript как «свойства», и процедуры, известные как «методы».

Некоторые из встроенных объектов JavaScript включают Math (используется для таких методов, как random, max и sin), JSON (используется для анализа данных JSON) и примитивные типы данных, такие как String, Array, Number и Boolean.

Всякий раз, когда вы полагаетесь на встроенные методы, прототипы или классы, вы, по сути, используете объектно-ориентированное программирование.

Функциональное программирование

FP основан на концепции «чистых функций», которые избегают общего состояния, изменяемых данных и побочных эффектов. Может показаться, что это много жаргона, но есть вероятность, что вы написали много чистых функций в своем коде.

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

Что касается общего состояния, вот краткий пример того, как состояние может изменить вывод функции, даже если ввод тот же. Давайте представим сценарий с двумя функциями: одна для сложения числа на 5, а вторая для умножения его на 5.

const num = {
  val: 1
}; 
const add5 = () => num.val += 5; 
const multiply5 = () => num.val *= 5;

Если мы вызовем add5 первым и multiply5 вторым, общий результат будет 30. Но если мы вызовем функции наоборот и зарегистрируем результат, мы получим нечто иное: 10.

Это противоречит принципу функционального программирования, так как результат функций зависит от контекста. Мы можем переписать приведенный выше код, чтобы результаты были предсказуемыми:

const num = {
  val: 1
};
const add5 = () => Object.assign({}, num, {val: num.val + 5}); 
const multiply5 = () => Object.assign({}, num, {val: num.val * 5});

Теперь значение num.val остается 1, и независимо от контекста add5(num) и multiply5(num) всегда будут давать один и тот же результат.

8. В чем разница между императивным и декларативным программированием?

Мы также можем подумать о разнице между ООП и ФП с точки зрения разницы между «императивным» и «декларативным» программированием.

Это общие термины, которые описывают общие характеристики нескольких разных парадигм программирования. FP - это пример декларативного программирования, а ООП - пример императивного программирования.

В основном, императивное программирование связано с тем, как вы что-то делаете. Он описывает шаги наиболее важным образом и характеризуется for и while циклами, if и switch операторами и т. Д.

const sumArray = array => {
  let result = 0;
  for (let i = 0; i < array.length; i++) { 
    result += array[i]
  }; 
  return result;
}

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

Вот декларативный подход к нашей функции sumArray() выше.

const sumArray = array => { return array.reduce((x, y) => x + y) };

9. Что такое наследование на основе прототипов?

Наконец, мы подошли к наследованию на основе прототипов. Существует несколько разных стилей объектно-ориентированного программирования, и в JavaScript используется наследование на основе прототипов. Система позволяет повторять поведение за счет использования существующих объектов, которые служат «прототипами».

Даже если идея прототипов для вас нова, вы столкнетесь с системой прототипов, используя встроенные методы. Например, функции, используемые для управления массивами, такими как map, reduce, splice и т. Д., Являются методами объекта Array.prototype. Фактически, каждый экземпляр массива (определенный с использованием квадратных скобок [] или, что более необычно, с использованием new Array()) наследуется от Array.prototype, поэтому такие методы, как map, reduce и splice, доступны по умолчанию.

То же самое верно практически для любого другого встроенного объекта, такого как строки и логические значения: только некоторые из них, такие как Infinity, NaN, null и undefined, не имеют свойств или методов.

В конце цепочки прототипов мы находим Object.prototype, и почти каждый объект в JavaScript является экземпляром Object.prototype: Array.prototype и String.prototype, например, оба наследуют свойства и методы от Object.prototype.

Чтобы добавить свойства и методы к объекту с использованием синтаксиса прототипа, вы можете просто инициировать объект как функцию и использовать ключевое слово prototype для добавления свойств и методов:

function Person() {};
Person.prototype.forename = "John";
Person.prototype.surname = "Smith";

Должен ли я отменять или расширять поведение прототипов?

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

Если вы хотите, чтобы несколько объектов имели одно и то же поведение, вы всегда можете создать собственный объект (или определить свой собственный «класс» или «подкласс»), который наследуется от встроенного прототипа, не внося никаких изменений в сам прототип. Если вы собираетесь работать с другими разработчиками, у них будут определенные ожидания относительно поведения JavaScript по умолчанию, и изменение этого поведения по умолчанию может легко привести к ошибкам.

Однако стоит отметить, что не все разделяют это решительное противодействие расширению встроенных прототипов. См., Например, эту статью Брендана Эйха, создателя JavaScript. В этой статье (от 2005 г.) Эйх предположил, что на самом деле прототип системы был построен - частично - для того, чтобы сделать расширения возможными!

В целом, я надеюсь, что эти вопросы помогли вам лучше понять JavaScript - как его основные функции, так и более необычные особенности - и что они помогут вам почувствовать себя более подготовленными к следующему собеседованию.

Если вы недавно проходили собеседование, я хотел бы узнать больше о вашем опыте! Какие вопросы возникли?

Или вы интервьюер, видите ли вы ценность в таких вопросах, как перечисленные выше? Или вы уделяете больше внимания другим областям, например техническим задачам или предыдущим проектам?