12 популярных вопросов с ответами на JavaScript собеседовании

2 года назад·4 мин. на чтение

Эта статья предназначена для junior разработчиков, которые хотят получить свою первую работу в качестве JavaScript разработчиков.

1. Что такое JavaScript и для чего он используется?

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

2. Что такое замыкание в JavaScript?

Замыкание — это функция, которая имеет доступ к переменным своей внешней функции. Это важно, потому что это позволяет внутренней функции «запоминать» переменные из своей внешней области, поэтому она может продолжать получать к ним доступ и манипулировать ими даже после того, как внешняя функция была завершена.
function outerFunction(x) {
  return function innerFunction(y) {
    return x + y;
  };
}

const add5 = outerFunction(5);
console.log(add5(3)); // 8

3. Как this работает в JavaScript?

this важное ключевое слово в JavaScript. Его значение определяется тем, как вызывается функция. Его можно задать явно с помощью call(), apply() или bind(). По умолчанию его значение внутри функции устанавливается в глобальный объект (в браузере это объект window), за исключением того, когда функция вызывается как метод объекта. В этом случае this указывает на объект, методом которого он является.

4. Что такое поднятие (hoisting) в JavaScript?

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

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

  • var: Переменные, объявленные с помощью var, имеют функциональную область действия, что означает, что к ним можно получить доступ только в рамках функции, в которой они были объявлены. Внешняя переменная затеняется, когда переменная с тем же именем объявляется во вложенной функции.
  • let: Пусть переменные имеют блочную область действия, что означает, что они доступны только в том блоке, в котором они объявлены. Объявление переменной с тем же именем во вложенном блоке приведет к затенению внешней переменной.
  • const: переменные, объявленные с помощью const, также как let, имеют область действия блока, но их нельзя переназначить.
Вот пример:
function example() {
  var x = 1;
  let y = 2;
  const z = 3;

  if (true) {
    var x = 4;
    let y = 5;
    const z = 6;
  }

  console.log(x); // 4
  console.log(y); // 2
  console.log(z); // 3
}

example();

6. Какова разница между == и ===?

Оператор равенства == выполняет принудительное приведение типа, что означает, что он попытается преобразовать операнды в один и тот же тип перед их сравнением. С другой стороны, оператор строгого равенства ===, не выполняет приведение типа. Он возвращает значение true только в том случае, если оба операнда имеют одинаковый тип и значение. Например:
console.log(1 == '1'); // true
console.log(1 === '1'); // false

7. В чем разница между null и undefined?

undefined указывает, что переменная была объявлена, но ей не было присвоено значение. Значение nullпредставляет собой преднамеренное отсутствие какого-либо значения объекта. Другими словами, null — это явно заданное значение, указывающее на отсутствие значения.

8. Что такое событие в JavaScript?

В JavaScript событие — это любое действие в браузере, например нажатие пользователем кнопки, загрузка страницы или обновленный элемент. Слушатели событий обычно обрабатывают события, позволяя разработчикам указывать функции, которые должны выполняться при возникновении события. Пример:
const button = document.querySelector('button');

button.addEventListener('click', () => {
  console.log('Button was clicked!');
});

9. В чем разница между синхронным и асинхронным кодом в JavaScript?

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

10. Что такое промисы (Promise) в JavaScript?

Объект Promise — это объект, представляющий успешное или неудачное завершение асинхронной операции. Promise позволяет регистрировать обратные вызовы для получения уведомлений о завершении или сбое асинхронной операции, а также обрабатывать ошибки более удобно и централизованно. Например:
const fetchData = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve('Data fetched!');
    }, 1000);
  });
};

fetchData()
  .then(data => {
    console.log(data);
  })
  .catch(error => {
    console.error(error);
  });

11. Что такое обратный вызов в JavaScript?

Функция обратного вызова передается в качестве аргумента другой функции и выполняется после завершения внешней функции. Разработчики могут использовать обратные вызовы для указания кода, который будет выполняться после завершения асинхронной операции. Пример:
const fetchData = (callback) => {
  setTimeout(() => {
    callback('Data fetched!');
  }, 1000);
};

fetchData(data => {
  console.log(data);
});

12. Что такое AJAX в JavaScript?

AJAX (асинхронный JavaScript и XML) — это метод выполнения асинхронных серверных запросов с веб-страницы без перезагрузки всей страницы. AJAX позволяет разработчикам динамически обновлять веб-страницу новыми данными без необходимости обновления страницы. Это достигается путем отправки HTTP-запроса из браузера на сервер и обновления только тех частей страницы, которые требуют его с ответом. Это улучшает взаимодействие с пользователем, поскольку страница не перезагружается, а обновляются только необходимые данные. Пример:
const xhr = new XMLHttpRequest();

xhr.open('GET', 'https://api.example.com/data');

xhr.onreadystatechange = () => {
  if (xhr.readyState === XMLHttpRequest.DONE) {
    console.log(xhr.responseText);
  }
};

xhr.send();
Еще больше вопросов с собеседований можно найти здесь.

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

2 года назад·5 мин. на чтение

В этой статье на простых и доступных примерах рассмотрим одну из концепций функционального программирования - Функтор.

Это серия статей о функциональном программировании:
  1. Парадигмы программирования
  2. Композиция
  3. Функторы (рассматривается в этой статье)
  4. Каррирование
  5. Чистые функции
  6. Функции первого класса

Что такое функтор?

Функтор (functor) это:
  • обертка над значением,
  • предоставляет интерфейс для преобразование (map),
  • подчиняется законам функтора (поговорим о них позже).

Примеры функторов

  • Массив (Array),
  • Промис (Promise).

Почему массив - функтор?

Вспомним определение функтора:
  • обертка над списком значений,
  • предоставляет интерфейс для преобразования - метод map,
  • подчиняется законам функтора.
[1, 2, 3]      // обернутое значение
  .map(        // интерфейс для преобразования значения
    x => x * 2
  )

Почему промис - функтор?

Промис это:
  • обертка над любым значением из JavaSctipt типов,
  • предоставляет интерфейс для преобразования - метод then,
  • подчиняется законам функтора.
const promise = new Promise((resolve, reject) => {
  resolve(
    { data: "value" } // обернутое значение, в данном случае объект
  )
});
 
promise
  .then(              // интерфейс для преобразования значения
    response => console.log(response)
  );

Что объединяет массив (или промис) и функтор?

Функтор - это паттерн проектирования, а Array и Promise - типы данных, которые основаны на этом паттерне.

Почему мы говорим, что массив и промис - функторы?

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

Где использовать функторы?

Немного поговорив о функторах и связав их с нашим повседневным использованием, было бы разумно рассмотреть их подробнее. Чтобы лучше понять идею функтора, создадим свои собственные функторы. Для начала рассмотрим такую задачу. Предположим, есть следующий кусочек данных.
{
  products: [
    {
      name: "All about JavaScript",
      type: "book",
      price: 22,
      discount: 20
    }
  ]
}

Постановка задачи

Найти финальную цену первого товара с учетом скидки. Если по какой-либо причине будут переданы неправильные данные, вывести строку "No data".

Шаги алгоритма

  1. Найти первый продукт со скидкой,
  2. Применить скидку,
  3. Продолжать проверку данных на валидность. Если данные не валидны - вернуть "No data".

Традиционное решение

const isProductWithDiscount = product => {
  return !isNaN(product.discount)
}
const findFirstDiscounted = products => {
  products.find(isProductWithDiscount)
}
 
const calcPriceAfterDiscount = product => {
  return product.price - product.discount
}
 
const findFinalPrice = (data, fallbackValue) => {
  if(!data || !data.products) return fallbackValue
 
  const discountedProduct = findFirstDiscounted(data.products)
  if(!discountedProduct) return fallbackValue
 
  return calcPriceAfterDiscount(discountedProduct)
}
 
findFinalPrice(data, "No data")

Комментарии к традиционному решению

Достоинства:
  • Атомарные логические единицы (isProductWithDiscount, findFirstDiscounted и calcPriceAfterDiscount),
  • Логику защищена от невалидных данных.
Что можно улучшить:
  • Cлишком много защитных проверок. (Защитное программирование (Defensive programming) является обязательным в любом отказоустойчивом программном обеспечении. Однако, в нашем коде 50% тела функции findFinalPrice — проверка на валидность данных. Это слишком много).
  • fallbackValue почти везде.

Почему нас волнуют эти улучшения?

Потому что данный код заставляет слишком в него вникать. Это негативно влияет на DX (Developer Experience) - уровень удовлетворенности разработчика от работы с кодом. Проанализируем код, чтобы прийти к лучшему решению. Части, которые мы стремимся улучшить, формируют паттерны (защита (defence) и откат (fallback)). Хорошо то, что эти части на самом деле цельные и атомарные. Мы должны иметь возможность абстрагировать этот паттерн в оболочку, которая могла бы обрабатывать эти крайние случаи вместо нас. Обертка позаботится о крайних случаях, а нам останется позаботиться только о бизнес-логике.

Функтор Maybe

Как мы обсуждали ранее, нам нужна только обертка, которая абстрагируется от обработки данных. Итак, роль функтора Maybe состоит в том, чтобы обернуть наши данные (потенциально невалидные данные) и обработать для нас крайние случаи.

Имплементация функтора Maybe

function Maybe(value) {
  const isNothing = () => {
    return value === null || value === undefined
  }
  
  const map = (fn) => {
    return isNothing() ? Maybe() : Maybe(fn(value))
  }
 
  const getValueOrFallback = {
    return (fallbackValue) => isNothing() ? fallbackValue : value;
  }
 
  return {
    map,
    getValueOrFallback,
  };
}

Пояснения к имплементации

  • isNothing проверяет валидно ли обернутое в функтор Maybe значение
  • map - интерфейс для преобразования обернутого значения, с помощью которого мы применяем функции с бизнес логикой к обернутому значению. map возвращает новое значение в другом экземпляре Maybe. Таким образом, мы можем сделать цепочку вызовов map - .map().map().map....
  • getValueOrFallback возвращает обернутое значение или запасное значение fallbackValue.

Как использовать функтор Maybe?

С валидными данными:
Maybe('Hello')
  .map(x => x.substring(1))
  .getValueOrFallback('fallback') // 'ello'
С невалидными данными:
Maybe(null)
  .map(x => x.substring(1))       // функция не будет запущена
  .getValueOrFallback('fallback') // 'fallback'
Функтор Maybe обработал крайние случаи вместо нас и не запустил функцию с невалидными данными. Нам нужно лишь позаботиться о бизнес логике. Таким образом, мы внедрили улучшение, о котором говорили в традиционном решении. Внедрим это решение в задачу.

Решение задачи с функтором Maybe

const isProductWithDiscount = product => {
  return !isNaN(product.discount)
}
const findFirstDiscounted = products => {
  return products.find(isProductWithDiscount)
}
const calcPriceAfterDiscount = product => {
  return product.price - product.discount
}
 
Maybe(data)
 .map((x) => x.products)
 .map(findFirstDiscounted)
 .map(calcPriceAfterDiscount)
 .getValueOrFallback("No data")

Комментарии к решению с функтором Maybe

Мы смогли улучшить традиционное решение при помощи функтора Maybe:
  • мы не защищаем код сами, вместо нас это делает функтор Maybe,
  • мы указали fallbackValue только один раз.
Как функтор Maybe соответствует определению функтора? Функтор Maybe это:
  1. обертка над любым значением из JavaScript типов,
  2. предоставляет интерфейс для преобразования - метод map,
  3. подчиняется законам функтора.

Законы функторов

Закон идентичности (Identity law)

Если при выполнении операции преобразования, значения в функторе преобразовываются сами на себя, результатом будет немодифицированный функтор.
const m1 = Maybe(value)
const m2 = Maybe(value).map(v => v)
// m1 и m2 эквивалентны

Закон композиции (Composition law)

Если две последовательные операции преобразования выполняются одна за другой с использованием двух функций, результат должен быть таким же, как и при одной операции отображения с одной функцией, что эквивалентно применению первой функции к результату второй.
const m1 = Maybe(value).map(v => f(g(v)))
const m2 = Maybe(value).map(v => g(v)).map(v => f(v))
// m1 и m2 эквивалентны

Зачем использовать функторы?

  • Абстракция над применением функции,
  • Усиление композиции функций,
  • Уменьшение количества защитного кода (как в функторе Maybe),
  • Более чистая структура кода,
  • Переменные более явно указывают на то, что мы ожидаем (что Maybe моделирует значение, которое может присутствовать, а может и не присутствовать).

Что означает Абстракция над применением функции?

То, что мы передаем функцию (т.е. x => x.products) в интерфейс преобразования (т.е. map) обертки (т.е. Maybe), и она знает, как позаботиться о себе (посредством своей внутренней реализации). Нас не интересуют детали реализации оболочки, которые она содержит (детали реализации скрыты), и тем не менее мы знаем, как использовать обертку (Array или Promise), используя их интерфейсы преобразования (map). И это на самом деле крайне важно в мире программирования. Это снижает планку того, как много мы, как программисты, должны понимать, чтобы иметь возможность что-то сделать. Функторы могут быть реализованы на любом языке, поддерживающем функции высшего порядка (а таких в наши дни большинство).

Почему функторы не используются повсеместно?

Просто потому, что мы к ним не привыкли. До .map.then) мы мутировали массивы или перебирали их значения вручную. Но как только мы обнаружили .map, мы начали адаптировать его в качестве нового инструмента преобразования. Я надеюсь, что, поняв ценность функторов, мы начнем чаще внедрять их в наши ежедневные задачи как привычный инструмент. Функтор Maybe - лишь пример функтора. Существует множество функторов, которые выполняет различные задачи. В этой статье мы рассмотрели самый простой из них, чтобы понять саму идею функторов.

Итоги

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