Мечтаете о должности JavaScript-разработчик? Держите ответы на каверзные и популярные вопросы на собеседовании. Примеры кода прилагаются.
JavaScript считается подходящим языком для начинающих. Отчасти из-за того, что массово используется для веб-разработки, а отчасти из-за особенностей языка, с которыми даже неидеальный код выполняется. Он не строгий, как другие языки: допускает пропуск точки с запятой или нежелание беспокоиться об управлении памятью.
Но когда JavaScript-разработчик готов к собеседованиям, он хочет быть уверенным в том, что знает тонкости языка и вещи, которые выполняются автоматически и «за кадром».
Мы рассмотрели распространённые вопросы на собеседовании JavaScript, среди которых есть пара каверзных! Конечно, собеседования разные: вам могут задать или не задать вопросы такого рода. Но лишних знаний не бывает ;)
Часть I. JavaScript-разработчик отвечает на каверзные вопросы
Эти вопросы полезны при подготовке: они раскрывают некоторые причудливые особенности JavaScript и выделяют решения, которые принимаются в первую очередь на этапе создания языка программирования.
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
возвращает false
?
Вкратце, это связано с тем, насколько точно 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-F).
Распространённый способ применения – права доступа к файлам для Unix-систем, где ровно восемь вариантов разрешений:
4 2 1 0 – – – нет разрешений 1 – – x только выполнение 2 – x – только запись 3 – x x запись и выполнение 4 x – – только чтение 5 x – x читать и выполнять 6 x x – чтение и запись 7 x x x читать, писать и выполнять
По тем же причинам они также используются для цифровых дисплеев.
Возвращение к вопросу
В JavaScript префикс 0
преобразует число в восьмеричное. Однако 8
не используется в восьмеричном виде, и любое число, содержащее 8
, будет автоматически преобразовано в десятичное число.
Следовательно, 018 - 017
по сути эквивалентно десятичному выражению 18 - 15
, потому что 017
– восьмеричное число, а 018
– десятичное.
Часть II: JavaScript-разработчик отвечает на общие вопросы
В этом разделе рассмотрим некоторые наиболее распространённые вопросы, на которые отвечает JavaScript-разработчик во время собеседования. Это те вещи, которые легко пропустить, когда только изучаете JavaScript, но знать которые полезно для написания наилучшего кода.
4. Чем отличается выражение функции (Function Expression) от объявления функции (Function Declaration)?
Объявление функции использует ключевое слово function
, за которым следует имя функции. Напротив, выражение функции начинается с var
, let
или const
, за которым следует имя функции и оператор присваивания =
. Вот примеры:
// Объявление функции function sum(x, y) { return x + y; }; // Выражение функции: ES5 var sum = function(x, y) { return x + y; }; // Выражение функции: ES6+ const sum = (x, y) => { return x + y };
При использовании главное отличие состоит в том, что объявления функций «поднимаются» (hoisted), а выражение функций – нет. Это означает, что объявления функций перемещаются интерпретатором JavaScript в верхнюю часть области видимости. Поэтому используем объявление функции и вызываем эту функцию в любом месте кода. Напротив, выражение функции вызывается только в линейной последовательности: определяется перед вызовом.
Сегодня JavaScript-разработчик предпочитает выражения функций по двум причинам:
- В первую очередь, функциональные выражения помогают создать более предсказуемую, структурированную кодовую базу. Конечно, этого можно достичь также и с объявлениями за счёт простоты избавления от грязного кода.
- Второе, используется синтаксис ES6 для выражений функций. Как правило, получается более кратко, а
let
иconst
предоставляют больше контроля над тем, можно ли переназначить переменную или нет, как увидим в следующем вопросе.
5. Чем отличаются var
, let
и const
?
Полагаем, что этот вопрос стал распространённым после выпуска ES6, когда компании в полной мере использовали новый синтаксис. var
– ключевое слово для объявления переменной с самого первого выпуска JavaScript. Но его недостатки привели к принятию двух новых ключевых слов в ES6: let
и const
.
У этих трёх ключевых слов разные подходы к присваиванию, поднятию (hoisting) и области видимости, поэтому рассмотрим каждое из свойств отдельно.
I) Присваивание
Главное различие заключается в том, что let
и var
можно переприсвоить, а const
– нет. Поэтому const
подходит для переменных, которые не требуют изменения, и предотвращает случайное переприсваивание. Обратите внимание, что const
допускает мутацию переменных. Это означает, что объявленные массив или объект могут измениться. Просто не получится переназначить саму переменную.
И let
, и var
допускают повторное присваивание, но, как увидим в следующих пунктах, у let
ощутимое преимущество по сравнению с var
. Поэтому выбираем его в большинстве или даже во всех случаях, когда переменная изменяется.
II) Поднятие (Hoisting)
Аналогично разнице между объявлениями и выражениями функций (обсуждалась выше), переменные, объявленные с использованием var
, всегда поднимаются в верхнюю часть соответствующей области видимости. А переменные, объявленные с использованием const
и let
, не поднимаются. Таким образом, var
больше подвергается ошибкам, таким как случайное переназначение. Возьмём пример:
var x = "глобальная область видимости"; function foo() { var x = "область видимости функции"; console.log(x); } foo(); // "область видимости функции" console.log(x); // "глобальная область видимости"
Здесь результат foo()
и console.log(x)
соответствует ожиданиям. Но что, если забыли второй var
?
var x = "global scope"; function foo() { x = "область видимости функции"; console.log(x); } foo(); // "область видимости функции" console.log(x); // "область видимости функции"
Несмотря на определение внутри функции, x = "область видимости функции"
перезаписывает глобальную переменную. Нужно повторить ключевое слово 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. Что произойдёт, если JavaScript-разработчик определит переменную без ключевого слова?
Что, если определим переменную без использования ключевого слова вообще? Технически, если x
ещё не определена раньше, то x = 1
будет сокращением для window.x = 1
. Это частая причина утечек памяти в JavaScript.
Чтобы избежать этого сокращения, используйте строгий режим – введённый в ES5 – напишите use strict
вверху документа или конкретной функции. Затем, при попытке объявить переменную без ключевого слова, получим ошибку: Uncaught SyntaxError: Unexpected indentifier
.
7. В чём разница между объектно-ориентированным программированием (ООП) и функциональным программированием (ФП)?
JavaScript – это мультипарадигмальный язык, что означает поддержку нескольких стилей программирования, включая событийно-ориентированный, функциональный и объектно-ориентированный.
Среди массы парадигм программирования сейчас выделяются два самых популярных стиля – функциональное программирование (ФП) и объектно-ориентированное программирование (ООП). И JavaScript поддерживает оба.
Объектно-ориентированное программирование
ООП базируется на понятии «объекты». Это структуры данных, которые содержат поля данных – известные в JavaScript как «свойства» – и процедуры – известные как «методы».
Некоторые из встроенных объектов JavaScript используют эту парадигму, включая Math
(для методов random
, max
и sin
), JSON (для анализа данных JSON) и примитивные типы данных, такие как String
, Array
, Number
и Boolean
.
Когда полагаемся на встроенные методы, прототипы или классы, то по существу используем объектно-ориентированное программирование.
Функциональное программирование
ФП основывается на концепции «чистых функций», в которых нет разделяемого состояния, изменяемых данных и побочных эффектов. Это кажется заумным, но вероятно, вы создавали много чистых функций в коде.
При одинаковых входных параметрах чистая функция возвращает один и тот же результат. У неё нет побочных эффектов: например, логирования в консоль или изменения внешней переменной, помимо возвращения результата.
Что касается разделяемого состояния, рассмотрим краткий пример, где такое состояние изменяет результат функции, даже если входные параметры совпадают. Добавим сценарий с двумя функциями: одна для прибавления 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. В чём разница между императивным и декларативным программированием?
Также подумаем о различиях ООП и ФП с точки зрения разницы между «императивным» и «декларативным» программированием.
Это общие термины, которые описывают общие характеристики нескольких парадигм программирования. ФП – пример декларативного программирования, а ООП – пример императивного программирования.
В базовом смысле императивное программирование касается того, как делать. Эта парадигма объясняет шаги конкретным образом и характеризуется циклами 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 использует прототип-ориентированное наследование. Система организует повторяющееся поведение с использованием действующих объектов – «прототипов».
Даже если идея прототипов окажется новой, 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 Брендан Эйх намекнул, что прототипная система на самом деле создавалась отчасти для возможности расширения!
В целом, надеемся, что эти вопросы помогли вам лучше понять JavaScript – как основные функции, так и особенности – и что помогут вам лучше подготовиться к следующему собеседованию.
Если вы JavaScript-разработчик
Недавно проходили собеседование? Хотим узнать больше о вашем опыте! Какие вопросы возникли?
Вдруг вы интервьюер, кажутся ли вам полезными вопросы наподобие перечисленных выше? Или уделяете больше внимания другим областям, таким как технические проблемы или предыдущие проекты?
Комментарии