Frontend

JavaScript #

Значения, ссылки и память #

Junior #

Что такое shallow copy и deep copy?

Shallow copy создает новый верхний объект, но вложенные объекты остаются общими. Deep copy рекурсивно создает независимые вложенные значения. Полное копирование может быть дорогим и не всегда имеет смысл для immutable updates.

Middle #

Чем примитивы отличаются от объектов?

Примитивы — string, number, bigint, boolean, symbol, undefined и null — являются неизменяемыми значениями. Объекты имеют identity, свойства и могут изменяться. Переменная с объектом хранит значение, позволяющее обратиться к этому объекту.

Что значит "передача по значению" в JavaScript?

При вызове функции параметр получает копию значения аргумента. Для примитива копируется сам примитив, для объекта — значение ссылки на объект. Переназначение параметра не меняет внешнюю переменную.

Передаются ли объекты по ссылке в JavaScript?

Точнее говорить: объекты тоже передаются по значению, но этим значением является ссылка на объект. Функция и вызывающий код получают две копии ссылки на один объект. Поэтому мутация объекта видна снаружи, а переназначение параметра — нет.

Чем отличается ссылка на объект от самого объекта?

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

Почему изменение свойства объекта внутри функции видно снаружи?

Скопированная ссылка параметра указывает на тот же объект, что и внешняя переменная. Запись свойства меняет этот общий объект. Новый объект при этом не создается.

Почему переназначение параметра внутри функции не меняет внешнюю переменную?

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

function changeUser(user) {
  user.name = 'Alex';
}

function replaceUser(user) {
  user = {name: 'Bob'};
}

const user = {name: 'Max'};

changeUser(user);
console.log(user.name); // Alex

replaceUser(user);
console.log(user.name); // Alex
Как работает сравнение объектов через ===?

=== для объектов сравнивает identity: указывают ли значения на один объект. Два отдельных объекта с одинаковыми свойствами не равны. Для структурного сравнения нужна отдельная функция с правилами для конкретного domain.

Чем отличаются spread, Object.assign и structuredClone?

Spread и Object.assign создают shallow copy enumerable own properties; первый удобен декларативно, второй умеет писать в заданный target. structuredClone выполняет deep clone поддерживаемых structured-clone типов и сохраняет cycles. Функции, DOM nodes и некоторые class semantics он не копирует.

Какие ограничения есть у JSON.parse(JSON.stringify(...)) для deep copy?

Метод теряет undefined, functions, symbols, prototype и типы вроде Date, Map, Set, а BigInt вызывает ошибку. Циклические ссылки также не поддерживаются. Он подходит только для заведомо JSON-совместимых данных.

Типы, функции и область видимости #

Junior #

Что такое область видимости в JS?

Область видимости в JavaScript — это правило, которое определяет, где переменная, функция или класс доступны в коде.

function test() {
  const name = 'Max';

  console.log(name); // доступна
}

console.log(name); // ошибка: name не видна снаружи

Основные виды scope в JS

  1. Global scope

Доступно везде в файле/программе:

const appName = 'My App';

function log() {
  console.log(appName);
}
  1. Function scope

var виден внутри всей функции:

function test() {
  var x = 1;

  if (true) {
    var y = 2;
  }

  console.log(y); // 2
}
  1. Block scope

let и const видны только внутри блока {}:

if (true) {
  const x = 1;
  let y = 2;
}

console.log(x); // ошибка
console.log(y); // ошибка
  1. Module scope

В ES-модулях переменные не попадают в global scope:

const value = 123;

export {value};

Middle #

Какие типы данных есть в JavaScript?

В JavaScript есть семь примитивных типов: string, number, bigint, boolean, undefined, symbol и null.

Все остальные значения относятся к типу object: обычные объекты, массивы, функции, даты и коллекции. Примитивы неизменяемы и сравниваются по значению, а переменные с объектами хранят ссылки.

// Примитивные

string; // "hello"
number; // 123, 3.14, NaN, Infinity
bigint; // 123n
boolean; // true / false
undefined; // значение не задано
null; // пустое значение
symbol; // уникальный идентификатор

// Ссылочный тип

object; // {}, [], function, Date, Map, Set и т.д.

Важно:

typeof null; // "object" — старая странность JS
typeof []; // "object"
typeof function () {}; // "function"
typeof Symbol('id'); // "symbol"

img.png

В чем разница между call и apply, bind в JS?

call и apply делают одно и то же: вызывают функцию с явно заданным this.

call

Аргументы передаются через запятую:

function greet(city, age) {
  console.log(`${this.name}, ${city}, ${age}`);
}

const user = {name: 'Max'};

greet.call(user, 'Moscow', 32);
// Max, Moscow, 32

// fn.call(thisArg, arg1, arg2, arg3);
function greet(city, age) {
  console.log(`${this.name}, ${city}, ${age}`);
}

const user = {name: 'Max'};

greet.apply(user, ['Moscow', 32]);
// Max, Moscow, 32

bind тоже работает с this, но не вызывает функцию сразу.

Он создает новую функцию, у которой this уже заранее привязан.

function greet(city) {
  console.log(`${this.name} from ${city}`);
}

const user = {name: 'Max'};

call — вызывает сразу. apply — вызывает сразу, но аргументы массивом. bind — НЕ вызывает сразу

const boundGreet = greet.bind(user);

boundGreet('Moscow');
// Max from Moscow
В чем отличие нативных (Native) объектов от хост-объектов (Host objects)?

Нативные объекты — часть спецификации языка. Они доступны нам вне зависимости от того, на каком клиенте исполняется наш код. Примеры: Array, Date и Math. Полный список нативных объектов.

var users = Array(); // Array — нативный объект

Встроенные (Built-in): Array, Date, Math, String, Promise, Object. Пользовательские: Объекты, создаваемые вами через new Object(), литералы {} или классы. Контекстные: Объект globalThis (или window в браузере, global в Node.js), Math и JSON.

Хост-объекты (Host objects)

Это объекты, предоставляемые средой выполнения (окружением), в которой запущен JavaScript (браузер, сервер Node.js и т.д.).

Они не являются частью самого языка, зависят от платформы и могут различаться.

В браузере: window, document, location, history, XMLHTTPRequest, fetch, элементы DOM, localStorage.

В Node.js: Объекты для работы с файловой системой (fs), процессами (process), операционной системой (os).

Массивы, объекты и даты #

Junior #

Что такое Object.fromEntries?

Object.fromEntries() преобразует iterable пар [key, value] в объект. Это обратная операция к Object.entries(); ее удобно сочетать с map, filter, Map и URLSearchParams.

const entries = [
  ['name', 'Max'],
  ['role', 'frontend'],
];

const user = Object.fromEntries(entries);
// { name: "Max", role: "frontend" }

Пример фильтрации свойств:

const params = {
  search: 'angular',
  page: 1,
  empty: undefined,
};

const cleaned = Object.fromEntries(Object.entries(params).filter(([, value]) => value !== undefined));

// { search: "angular", page: 1 }
Что такое Array.prototype.reduce?

reduce() последовательно сворачивает массив в одно значение. Результатом может быть число, объект, массив или Map.

const total = [10, 20, 30].reduce((sum, value) => sum + value, 0);
// 60

Initial value стоит задавать явно, особенно если массив может быть пустым:

const byYear = operations.reduce<Record<string, typeof operations>>((groups, operation) => {
  const year = operation.date.slice(0, 4);

  groups[year] ??= [];
  groups[year].push(operation);

  return groups;
}, {});

Не стоит использовать reduce(), если map, filter, some, every или find выражают намерение понятнее.

Что такое sparse array?

Sparse array, разреженный массив, содержит пустые слоты, в которых нет свойства с соответствующим индексом. Это не то же самое, что явное значение undefined.

const sparse = [1, , 3];

0 in sparse; // true
1 in sparse; // false
sparse.length; // 3

Методы ведут себя по-разному: map() сохраняет пустой слот, filter(), forEach() и flatMap() не вызывают callback для него, а spread и Array.from() превращают слот в undefined. В прикладном коде разреженных массивов обычно избегают.

Что такое ISO-формат даты?

ISO 8601 - распространенный стандарт записи даты и времени. В JavaScript часто встречается формат YYYY-MM-DDTHH:mm:ss.sssZ.

const iso = '2026-06-20T10:30:00.000Z';

T разделяет дату и время, а Z обозначает UTC. Важно не путать UTC с локальным временем пользователя.

Middle #

Что такое Object.groupBy и когда его использовать?

Object.groupBy() группирует элементы iterable по ключу, который возвращает callback. Результат - объект, поэтому API удобно использовать, когда ключи группировки можно представить строками или symbols.

const operations = [
  {date: '2017-07-31', amount: 5422},
  {date: '2018-03-31', amount: 5654},
  {date: '2017-08-31', amount: 5451},
];

const byYear = Object.groupBy(operations, ({date}) => date.slice(0, 4));

// {
//   "2017": [...],
//   "2018": [...]
// }

Object.groupBy() подходит, например, для группировки операций по году или задач по статусу. Возвращаемый объект имеет null в качестве prototype, поэтому методы вроде hasOwnProperty() у него напрямую недоступны.

Чем Object.groupBy отличается от Map.groupBy?

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

const active = {label: 'active'};
const archived = {label: 'archived'};

const users = [
  {name: 'Max', status: active},
  {name: 'Anna', status: archived},
  {name: 'Kate', status: active},
];

const grouped = Map.groupBy(users, ({status}) => status);

grouped.get(active);
// [{ name: "Max", ... }, { name: "Kate", ... }]

Выбор зависит от того, какие ключи нужны и будет ли результат дальше использоваться как объект или Map.

Что делает Object.hasOwn?

Object.hasOwn(object, key) проверяет, есть ли свойство непосредственно у объекта, а не в его prototype chain.

const user = Object.create({role: 'admin'}) as {
  name?: string;
  role: string;
};

user.name = 'Max';

Object.hasOwn(user, 'name'); // true
Object.hasOwn(user, 'role'); // false
'role' in user; // true

В отличие от object.hasOwnProperty(), статический метод работает с объектами без prototype и объектами, переопределившими hasOwnProperty.

Чем Object.hasOwn отличается от оператора in?

Object.hasOwn() проверяет только собственное свойство. Оператор in ищет ключ и в самом объекте, и во всей prototype chain.

Object.hasOwn() подходит для проверки входных данных и словарей. in полезен, когда наличие унаследованного свойства тоже является частью контракта, а в TypeScript еще используется для narrowing union types.

Чем Object.entries отличается от Object.fromEntries?

Object.entries() превращает собственные enumerable string-keyed свойства объекта в массив пар [key, value]. Object.fromEntries() выполняет обратное преобразование.

const user = {name: 'Max', role: 'frontend'};
const entries = Object.entries(user);
const copy = Object.fromEntries(entries);

Symbols не попадают в Object.entries().

Что возвращает Object.keys и в каком порядке?

Object.keys() возвращает массив собственных enumerable строковых ключей. Symbol-ключи в результат не входят.

Integer-like ключи идут по возрастанию, остальные строковые ключи - в порядке добавления:

const value = {10: 'ten', 2: 'two', name: 'Max'};

Object.keys(value); // ["2", "10", "name"]

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

Чем flat отличается от flatMap?

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

const nested = [[1, 2], [3]];
nested.flat(); // [1, 2, 3]

const users = [
  {name: 'Max', roles: ['admin', 'editor']},
  {name: 'Anna', roles: ['viewer']},
];

const roles = users.flatMap(({roles}) => roles);
// ["admin", "editor", "viewer"]
Когда использовать flatMap вместо map().flat()?

flatMap() короче выражает преобразование, при котором один входной элемент дает ноль, один или несколько выходных элементов.

const values = [1, -1, 2];
const positive = values.flatMap((value) => (value > 0 ? [value] : []));
// [1, 2]

flatMap() раскрывает только один уровень. Если нужна другая глубина или преобразование и flatten являются отдельными шагами, понятнее использовать map().flat(depth).

Чем toSorted отличается от sort?

sort() сортирует массив на месте, а toSorted() возвращает новый массив. toSorted() удобнее для immutable state, Angular signals и Redux-подобных подходов.

const numbers = [10, 2, 30];
const sorted = numbers.toSorted((first, second) => first - second);

console.log(numbers); // [10, 2, 30]
console.log(sorted); // [2, 10, 30]

sort() изменяет исходный массив:

const numbers = [10, 2, 30];

numbers.sort((first, second) => first - second);

console.log(numbers); // [2, 10, 30]

Для чисел нужен comparator, иначе значения сортируются как строки.

Чем reverse отличается от toReversed?

reverse() меняет порядок элементов исходного массива. toReversed() возвращает новый массив и не мутирует источник.

const source = [1, 2, 3];
const reversed = source.toReversed();

console.log(source); // [1, 2, 3]
console.log(reversed); // [3, 2, 1]
Чем splice отличается от toSpliced?

splice() изменяет исходный массив и возвращает удаленные элементы. toSpliced() возвращает новый массив с примененным изменением.

const source = ['a', 'b', 'c'];
const updated = source.toSpliced(1, 1, 'x');

console.log(source); // ["a", "b", "c"]
console.log(updated); // ["a", "x", "c"]
Что делает array.with?

array.with(index, value) возвращает копию массива с замененным элементом. Исходный массив не меняется; поддерживаются и отрицательные индексы.

const source = ['draft', 'review', 'done'];
const updated = source.with(1, 'approved');

console.log(source); // ["draft", "review", "done"]
console.log(updated); // ["draft", "approved", "done"]

Недопустимый индекс приводит к RangeError.

Почему immutable-методы массивов полезны в Angular?

toSorted(), toReversed(), toSpliced() и with() создают новую ссылку. Это делает обновление signals, OnPush-компонентов и store предсказуемым.

readonly users = signal<ReadonlyArray<User>>([]);

sortByName(): void {
  this.users.update((users) =>
    users.toSorted((first, second) => first.name.localeCompare(second.name)),
  );
}

Мутация массива на месте может не создать ожидаемого реактивного обновления и усложняет сравнение предыдущего и нового состояния.

Какие базовые API есть у Date?

Date хранит момент времени, а не календарную дату без времени.

const now = new Date();

now.getFullYear();
now.getMonth(); // 0-11
now.getDate(); // День месяца

Date.now(); // Текущий timestamp в миллисекундах
now.getTime(); // Timestamp конкретной даты

getFullYear(), getMonth() и getDate() используют локальную таймзону. Их UTC-варианты: getUTCFullYear(), getUTCMonth() и getUTCDate().

Что делает Date.prototype.toISOString?

toISOString() возвращает строку в ISO-подобном формате YYYY-MM-DDTHH:mm:ss.sssZ. Результат всегда представлен в UTC.

const date = new Date('2026-06-20T10:30:00+03:00');

date.toISOString();
// "2026-06-20T07:30:00.000Z"

Метод удобен для API, логов, сериализации и приведения моментов времени к единому формату.

Какие ошибки часто допускают при работе с Date?
  • Забывают, что getMonth() возвращает значения от 0 до 11.
  • Путают локальное время и UTC.
  • Парсят строки нестандартного формата с зависимым от среды результатом.
  • Сравнивают даты как локализованные строки.
  • Не учитывают, что setDate(), setMonth() и setFullYear() мутируют объект.
  • Ожидают, что Date хранит календарную дату без времени.
const date = new Date();

date.setDate(date.getDate() + 1); // Мутирует исходный объект

Для более сложной работы с датами развивается Temporal, но базовые вопросы обычно сфокусированы на Date.

Как сравнивать даты в JavaScript?

Моменты времени удобно сравнивать по timestamp через getTime():

const first = new Date('2026-06-20T10:00:00.000Z');
const second = new Date('2026-06-20T12:00:00.000Z');

first.getTime() < second.getTime(); // true

Для API моменты времени обычно передают в ISO/UTC. Если значение является календарной датой без времени, например днем рождения, его часто безопаснее хранить отдельной строкой YYYY-MM-DD, чтобы не получить сдвиг из-за таймзоны.

Promise и асинхронность #

Junior #

Что такое Promise.try?

Promise.try(callback, ...args) синхронно вызывает callback и возвращает promise. Обычное значение становится fulfillment, возвращенный promise ожидается, а синхронная ошибка превращается в rejection.

const result = await Promise.try(parseConfig, rawConfig);

API удобно на границе, где callback может быть синхронным или асинхронным. Это новый стандартный метод, поэтому перед использованием нужно проверить поддержку целевых browsers и runtime.

Middle #

Чем Promise.all отличается от Promise.allSettled?

Promise.all() успешно завершается, когда выполнены все promises, и возвращает значения в исходном порядке. При первом rejection итоговый promise сразу отклоняется: это fail-fast поведение.

Promise.allSettled() ждет завершения всех операций и возвращает для каждой { status, value } или { status, reason }. Он подходит для частично успешных независимых запросов.

const results = await Promise.allSettled([loadProfile(), loadRecommendations()]);

const successful = results.filter((result): result is PromiseFulfilledResult<unknown> => result.status === 'fulfilled');

Promise.all([]) возвращает fulfilled promise со значением []; обработчик then или продолжение после await все равно выполняется асинхронно.

Что произойдет при ошибке внутри Promise.all?

Итоговый promise отклонится с причиной первого обнаруженного rejection. Остальные запущенные операции автоматически не отменяются и могут продолжить работу.

Если допустим частичный результат, используют Promise.allSettled() или обрабатывают ошибку каждого promise отдельно. Если операции нужно остановить, им передают общий AbortSignal.

Когда использовать Promise.race?

Promise.race() возвращает результат первого settled promise: как fulfilled, так и rejected. Метод подходит для выбора первого ответа, соревнования альтернативных источников или timeout-сигнала.

Важно: проигравшие операции автоматически не отменяются.

Middle+ or Senior #

Чем Promise.any отличается от Promise.race?

Promise.any() возвращает первый fulfilled результат и игнорирует промежуточные rejections. Если отклонены все promises, он завершается AggregateError.

Promise.race() завершается при первом settled результате, поэтому первый rejection сразу отклонит итоговый promise.

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

Что такое Promise.withResolvers?

Promise.withResolvers<T>() создает promise и отдельно возвращает связанные функции resolve и reject.

const {promise, resolve, reject} = Promise.withResolvers<string>();

button.addEventListener('click', () => resolve('confirmed'), {once: true});

const result = await promise;

Метод удобен при адаптации callback/event API, но внешнее управление promise усложняет жизненный цикл. Для обычной последовательной логики чаще проще async/await.

URL и query params #

Junior #

Что такое URL и URLSearchParams?

URL разбирает и изменяет адрес через структурированные свойства. URLSearchParams работает с query parameters и корректно кодирует имена и значения.

const url = new URL('/users', 'https://example.com');

url.searchParams.set('page', '2');
url.searchParams.set('search', 'Angular & RxJS');

url.toString();
// "https://example.com/users?page=2&search=Angular+%26+RxJS"

Это безопаснее и понятнее ручной конкатенации query string.

Как добавлять, изменять и удалять query parameters?
const params = new URLSearchParams('page=1');

params.set('page', '2');
params.append('tag', 'angular');
params.delete('page');

params.toString(); // "tag=angular"

get(name) возвращает первое значение или null, если параметра нет. Все значения хранятся как строки.

Middle #

Чем URLSearchParams.append отличается от set?

append() добавляет еще одно значение и сохраняет существующие. set() заменяет все значения параметра одним новым значением.

const params = new URLSearchParams();

params.append('tag', 'angular');
params.append('tag', 'rxjs');

params.get('tag'); // "angular"
params.getAll('tag'); // ["angular", "rxjs"]

Для multi-value параметров используют append() и getAll().

Отмена асинхронных операций #

Junior #

Что такое AbortController и AbortSignal?

AbortController управляет отменой, а его signal передается поддерживающей отмену операции. Вызов abort() переводит signal в состояние aborted и сообщает причину наблюдателям.

const controller = new AbortController();

const request = fetch('/api/users', {
  signal: controller.signal,
});

controller.abort();
await request; // Rejection с AbortError

Один signal можно передать нескольким связанным операциям.

Middle #

Как сделать timeout для fetch?

Современный вариант использует AbortSignal.timeout():

const response = await fetch('/api/users', {
  signal: AbortSignal.timeout(5_000),
});

Если нужен ручной контроль, создают AbortController, вызывают abort() через timer и очищают timer в finally.

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5_000);

try {
  return await fetch('/api/users', {signal: controller.signal});
} finally {
  clearTimeout(timeoutId);
}
Чем отмена запроса отличается от игнорирования результата?

Игнорирование результата не останавливает сетевую работу и обработку ответа. Настоящая отмена через AbortSignal позволяет поддерживающему API прекратить ненужную операцию и освободить ресурсы раньше.

В Angular это встречается в autocomplete, навигации между страницами и уничтожении компонентов. HttpClient Observable отменяет запрос при unsubscribe; switchMap использует это для отмены предыдущего поиска. Для fetch и других Web APIs передают AbortSignal.

HTTP и REST #

Junior #

Что такое REST?

REST (Representational State Transfer) — это набор правил и принципов для построения взаимодействия между программами (клиентом и сервером) через интернет. Обычно клиент (например, браузер или мобильное приложение) запрашивает данные у сервера, а тот их возвращает, чаще всего по протоколу HTTP.

Ключевые принципы REST Клиент-серверная модель:

  1. Четкое разделение: сервер хранит и обрабатывает данные, а клиент занимается интерфейсом и отправкой запросов.
  2. Отсутствие состояния (Stateless): Каждый запрос от клиента содержит всю необходимую информацию для его обработки. Сервер не «помнит» клиента между запросами.
  3. Использование стандартных методов HTTP:
    • Для управления данными используются определенные запросы (так называемый CRUD):
    • GET — получение данных
    • POST — создание новых данных
    • PUT или PATCH — обновление существующих данных
    • DELETE — удаление данных
  4. Уникальные адреса (URI): Каждый ресурс или объект (пользователь, товар, статья) имеет свой уникальный адрес в сети (например, https://site.com).

Middle #

Что было до REST и после?

REST (Representational State Transfer) — это набор правил и принципов для построения взаимодействия между программами (клиентом и сервером) через интернет. Обычно клиент (например, браузер или мобильное приложение) запрашивает данные у сервера, а тот их возвращает, чаще всего по протоколу HTTP.

Ключевые принципы REST Клиент-серверная модель:

  1. RPC

Идея: клиент вызывает удаленную функцию как обычную функцию.

userService.getUser(123);

На уровне сети это превращалось в запрос к серверу.

Минус: клиент часто сильно завязан на серверные методы. То есть API выглядит как набор команд.

  1. SOAP

SOAP — более формальный XML-based подход.

<soap:Envelope>
  <soap:Body>
    <GetUser>
      <UserId>123</UserId>
    </GetUser>
  </soap:Body>
</soap:Envelope>

Особенности:

  • XML;
  • строгие схемы;
  • много формальности;
  • часто использовался в enterprise, банках, госке, больших системах.

Минус: тяжеловесно, много boilerplate.

  1. REST

REST стал популярным как более простой HTTP-подход.

call getUser(123)

Появляется ресурс:

GET / users / 123;

Вместо:

call deleteUser(123)

REST-стиль:

DELETE /users/123

Главная идея REST: API строится вокруг ресурсов, а HTTP-методы описывают действие.

REST никуда не исчез. Он до сих пор основной стандарт для обычных web API. Но рядом появились другие подходы.

  1. GraphQL

Идея: клиент сам говорит, какие поля ему нужны.

query {
  user(id: 123) {
    name
    avatar
    posts {
      title
    }
  }
}

Плюсы:

  • меньше лишних данных;
  • удобно для сложных UI;
  • frontend сам собирает нужную форму данных.

Минусы:

  • сложнее кеширование;
  • сложнее backend;
  • легко сделать тяжелый запрос.

Хорошо подходит, когда UI сложный и REST начинает плодить много endpoint-ов.

  1. gRPC

Идея: быстрые типизированные контракты между сервисами. Обычно используется не browser ↔ backend, а backend ↔ backend.

service UserService {
  rpc GetUser (GetUserRequest) returns (User);
}

Плюсы:

  • быстро;
  • строго типизировано;
  • хорошо для микросервисов.

Минусы:

  • менее удобно напрямую из браузера;
  • хуже читается человеком, чем JSON/REST.

DOM, SSR и hydration #

Middle+ or Senior #

Что такое hydration в JavaScript?

Hydration — это процесс, при котором JavaScript подключает интерактивное поведение к уже существующей HTML-разметке, обычно полученной с сервера через SSR или prerender. Браузер сначала показывает готовый HTML, а затем клиентский код находит нужные DOM-узлы, восстанавливает состояние и навешивает обработчики событий.

Упрощенный пример:

<button
  id="counter"
  data-count="0"
>
  0
</button>

<script type="module">
  const button = document.querySelector('#counter');

  if (button instanceof HTMLButtonElement) {
    let count = Number(button.dataset.count ?? 0);

    button.addEventListener('click', () => {
      count += 1;
      button.dataset.count = String(count);
      button.textContent = String(count);
    });
  }
</script>

До выполнения скрипта кнопка уже видна пользователю, но еще не интерактивна. После hydration обработчик click подключается к существующей кнопке, а не создает ее заново.

Главное требование — клиентский код должен ожидать такую же начальную разметку, какую отдал сервер. Если сервер отрендерил 0, а клиент при старте ожидает 1, framework может получить hydration mismatch и пересоздать часть DOM.

Event Loop #

Middle+ or Senior #

Чем отличается queueMicrotask от setTimeout?
  • queueMicrotask выполняет код после текущего синхронного кода, но до рендера и до setTimeout.
  • setTimeout выполняет код в следующей macrotask, то есть позже: после microtasks, часто после рендера.
console.log('1');

setTimeout(() => console.log('setTimeout'), 0);

queueMicrotask(() => console.log('queueMicrotask'));

console.log('2');

Вывод:

1;
2;
queueMicrotask;
setTimeout;
Что такое Event loop?

JavaScript в браузере выполняется в основном в одном потоке. Поэтому ему нужен диспетчер, который по очереди обрабатывает:

  • обычный синхронный код;
  • клики, ввод, события;
  • setTimeout;
  • Promise.then;
  • queueMicrotask;
  • рендер страницы.
┌──────────────────────────────┐
│        Call Stack             │
│  выполняется текущий JS-код   │
└───────────────┬──────────────┘
                │
                ▼
┌──────────────────────────────┐
│       Microtask Queue         │
│  Promise.then                 │
│  queueMicrotask               │
└───────────────┬──────────────┘
                │
                ▼
┌──────────────────────────────┐
│      Browser Rendering        │
│  layout / paint / update UI   │
└───────────────┬──────────────┘
                │
                ▼
┌──────────────────────────────┐
│         Task Queue            │
│  setTimeout                   │
│  click                        │
│  input                        │
│  network events               │
└───────────────┬──────────────┘
                │
                └─────── снова в Call Stack
console.log('1');

setTimeout(() => {
  console.log('2');
}, 0);

Promise.resolve().then(() => {
  console.log('3');
});

queueMicrotask(() => {
  console.log('4');
});

console.log('5');
1;
5;
3;
4;
2;

Расширенные основы JavaScript #

Junior #

Что такое this и расскажите про область видимости?

Область видимости определяет, где доступна переменная. В JavaScript есть глобальная, модульная, функциональная и блочная область видимости. let и const имеют блочную область, var — функциональную.

this определяется способом вызова функции:

  • obj.method()this обычно равен obj;
  • fn.call(value) / apply / bind — значение задается явно;
  • new Constructor()this указывает на создаваемый объект;
  • при обычном вызове в strict mode — undefined;
  • стрелочная функция не имеет собственного this и берет его из внешней области.
class Counter {
  count = 0;

  increment = (): void => {
    this.count += 1;
  };
}

Нельзя определять this только по месту объявления обычной функции: важно место и форма вызова.

Что такое Promise и для чего используется в JS?

Promise<T> представляет будущий результат одной асинхронной операции. У него есть состояния pending, fulfilled и rejected; после выполнения состояние изменить нельзя.

fetch('/api/users')
  .then((response) => response.json())
  .catch((error: unknown) => handleError(error))
  .finally(() => hideLoader());

Обработчики then, catch и finally выполняются как microtasks. async/await — более читаемый синтаксис поверх Promise.

Для параллельной работы есть Promise.all, allSettled, race и any. Сам Promise не предоставляет универсальной отмены операции; для fetch используют AbortController.

Что такое макро и микро задачи в JS?

Термин task часто неформально называют macrotask.

  • Tasks: выполнение скрипта, setTimeout, setInterval, события UI, сетевые callbacks.
  • Microtasks: обработчики Promise, queueMicrotask, MutationObserver.

После завершения текущей task движок полностью очищает очередь microtasks и только затем может выполнить рендер и перейти к следующей task.

Если microtasks непрерывно добавляют новые microtasks, они могут задержать рендер и обработку событий. Поэтому тяжелую работу нельзя бесконечно дробить только через Promise или queueMicrotask.

Что такое класс и интерфейс?

Класс описывает создание объектов, их состояние и поведение. Класс существует во время выполнения JavaScript.

Интерфейс TypeScript описывает контракт формы значения и используется только при проверке типов. После компиляции интерфейс исчезает.

interface UserRepository {
  findById(id: string): Promise<User | null>;
}

class HttpUserRepository implements UserRepository {
  async findById(id: string): Promise<User | null> {
    return loadUser(id);
  }
}

Интерфейс нельзя напрямую использовать как Angular DI-токен, потому что его нет в runtime. Для DI используют класс, InjectionToken или другой runtime-токен.

Что такое конструктор класса?

Конструктор — специальный метод, который выполняется при создании экземпляра через new. Он инициализирует обязательное состояние объекта и принимает зависимости или параметры.

class User {
  constructor(
    readonly id: string,
    readonly name: string,
  ) {}
}

В производном классе до обращения к this нужно вызвать super().

В Angular конструктор класса не является lifecycle hook. Для компонентов он должен оставаться простым: DI и базовая инициализация выполняются в конструкторе, а логика, зависящая от входных данных, размещается в соответствующем lifecycle hook или реактивной модели.

Middle #

В чем отличие var от const, let?
  • var имеет функциональную область видимости, допускает повторное объявление и поднимается с начальным значением undefined.
  • let имеет блочную область видимости, допускает повторное присваивание, но не повторное объявление в том же блоке.
  • const имеет блочную область видимости и требует значение при объявлении; повторное присваивание запрещено.

let и const тоже поднимаются, но до инициализации находятся в temporal dead zone.

const запрещает изменить саму ссылку, но не делает объект неизменяемым:

const user = {name: 'Ann'};
user.name = 'Kate'; // Допустимо

По умолчанию используют const, а let — только когда переменную действительно нужно переназначить. var в современном коде обычно не используют.

Как устроено прототипное наследование в JavaScript?

Каждый обычный объект может иметь внутреннюю ссылку [[Prototype]] на другой объект. Если свойства нет у самого объекта, JavaScript ищет его в прототипе, затем в прототипе прототипа и так далее до null. Это и есть цепочка прототипов.

const animal = {moves: true};
const dog = Object.create(animal);

dog.barks = true;
console.log(dog.moves); // Найдено в прототипе

Синтаксис class появился в ECMAScript 2015. Он делает создание конструкторов, методов и наследования удобнее, но в основе по-прежнему лежит прототипная модель.

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

Почему Set.has часто лучше Array.includes для частых проверок?

Array.includes() проходит массив линейно, поэтому одна проверка стоит O(n). Если проверять принадлежность внутри filter() или map() для большого списка, легко получить лишний вложенный перебор.

Set.has() в среднем работает за O(1), но сначала нужно построить Set. Поэтому замена полезна, когда набор проверяется много раз или достаточно большой.

const visibleIds = new Set(filters.visibleIds);

const visibleRows = rows.filter((row) => visibleIds.has(row.id));

Для одного короткого списка includes() часто читабельнее и быстрее за счет меньшего overhead. Решение стоит подтверждать профилем, если это hot path.

Когда Map лучше обычного объекта?

Map полезен, когда ключи не только строки, когда важны частые set/get/delete, когда нужно сохранять порядок вставки или явно хранить коллекцию пар ключ-значение.

Обычный объект удобен для JSON-like данных и фиксированной формы:

const user = {
  id: '42',
  name: 'Ann',
};

Map лучше подходит для индекса:

const usersById = new Map(users.map((user) => [user.id, user]));
const currentUser = usersById.get(currentUserId);

В Angular-состоянии часто хранят и ids: string[] для порядка, и entities: Map<string, User> или plain object для быстрого доступа по id.

Какие ошибки часто бывают в comparator для sort?

Comparator должен возвращать отрицательное число, ноль или положительное число и быть согласованным. Частые ошибки: возвращать boolean, мутировать элементы, зависеть от внешнего меняющегося состояния, забывать числовой comparator или не обрабатывать равенство.

const wrong = prices.toSorted((first, second) => (first.price > second.price ? 1 : -1));

const correct = prices.toSorted((first, second) => first.price - second.price);

Для строк лучше использовать localeCompare() или Intl.Collator, особенно если важны язык, регистр и числа внутри строк. Для больших таблиц сортировку иногда переносят на сервер или делают в Web Worker, чтобы не блокировать main thread.

Почему string.length в JavaScript не всегда равен количеству видимых символов?

string.length считает UTF-16 code units, а не пользовательские символы. Emoji, некоторые редкие символы и символы с combining marks могут занимать несколько code units.

'😄'.length; // 2
Array.from('😄').length; // 1 code point

Даже code point не всегда равен видимому символу: один grapheme cluster может состоять из нескольких code points. Поэтому обрезка пользовательского имени, textarea limit или preview текста должны учитывать Unicode, если продукт работает с emoji и разными языками. Для пользовательских символов можно использовать Intl.Segmenter, когда он доступен.

Почему 0.1 + 0.2 !== 0.3?

JavaScript number использует IEEE 754 double precision floating point. Многие десятичные дроби нельзя представить точно в двоичной форме, поэтому результат вычисления содержит маленькую погрешность.

0.1 + 0.2; // 0.30000000000000004

Для отображения используют округление, например Intl.NumberFormat. Для денег лучше хранить minor units, например копейки или центы целым числом, либо использовать decimal-библиотеку на границе расчетов.

const totalCents = 10 + 20;
const formatted = new Intl.NumberFormat('ru-RU', {
  style: 'currency',
  currency: 'RUB',
}).format(totalCents / 100);
Какие ограничения есть у bitwise operators в JavaScript?

Bitwise operators приводят значения к 32-bit signed integer, кроме >>>, который возвращает unsigned 32-bit результат. Из-за этого они могут неожиданно обрезать большие числа и дробную часть.

Math.pow(2, 40) | 0; // 0

Битовые операции уместны для flags, binary protocols, canvas/image processing и TypedArray. Для обычного UI-кода читаемая структура вроде Set<Permission> часто лучше битовой маски.

Middle+ or Senior #

Как устроена память в JavaScript (memory heap, memory stack)?

Упрощенная модель состоит из call stack и heap:

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

Сборщик мусора освобождает объекты, которые больше недостижимы от корней приложения. Основная идея современных сборщиков мусора — mark and sweep.

Типичные причины утечек: забытые подписки и обработчики, бесконечно растущий кеш, таймеры, замыкания и ссылки на удаленные DOM-узлы. В Angular для подписок можно использовать AsyncPipe, toSignal() или takeUntilDestroyed().

Что такое call-stack, task-queue (приведите примеры работы)?

Call stack хранит активные вызовы функций. JavaScript выполняет верхний frame стека и снимает его после возврата из функции.

Task queue содержит готовые к выполнению задачи: таймеры, DOM-события и другие callbacks. Event loop передает следующую задачу в стек, когда стек пуст и обработаны microtasks.

console.log('A');

setTimeout(() => console.log('B'), 0);
Promise.resolve().then(() => console.log('C'));

console.log('D');

Порядок вывода: A, D, C, B. Синхронный код выполняется первым, затем microtasks, затем следующая task.

Почему event loop не спасает от плохой алгоритмической сложности?

Event loop умеет чередовать задачи, но синхронный JavaScript все равно выполняется на main thread до завершения текущей task. Если обработчик клика сортирует, фильтрует и рендерит огромный список за сотни миллисекунд, браузер не сможет обработать input и paint посередине этой работы.

Решения зависят от причины: улучшить алгоритм, заранее построить индекс через Map, добавить pagination/virtual scroll, разбить работу на chunks, перенести CPU-bound часть в Web Worker или выполнить тяжелую агрегацию на сервере.

JavaScript interview questions #

Junior #

Что такое event delegation?

Event delegation — это прием, при котором обработчик события ставят на общего родителя, а не на каждый дочерний элемент. Событие всплывает вверх по DOM, а обработчик определяет исходный элемент через event.target. Это уменьшает число listeners и удобно для динамических списков; в Angular template bindings обычно скрывают детали, но понимание DOM events помогает писать directives и интеграции.

list.addEventListener('click', (event) => {
  const button = event.target.closest('button[data-id]');

  if (!button || !list.contains(button)) {
    return;
  }

  selectItem(button.dataset.id);
});
Как работает prototypal inheritance?

Объект имеет internal prototype, по которому JavaScript ищет свойство, если его нет на самом объекте. Этот поиск идет по prototype chain до null. class в JavaScript — синтаксис поверх prototype model, а не отдельная модель наследования.

Что такое closure и когда она полезна?

Closure возникает, когда функция сохраняет доступ к переменным внешней lexical scope после завершения внешней функции. Она полезна для callbacks, factories, инкапсуляции состояния и partial application. Нужно помнить, что closure удерживает ссылки на значения и может продлить их жизнь в памяти.

Что такое hoisting?

Hoisting — поведение, при котором declarations обрабатываются до выполнения кода в scope. Function declarations доступны до объявления, var создается со значением undefined, а let, const и class находятся в temporal dead zone до строки объявления. Поэтому порядок кода все равно важен для читаемости и корректности.

Что такое type coercion?

Type coercion — неявное или явное преобразование значения к другому типу. JavaScript делает это в арифметике, сравнениях, string concatenation и boolean context. Неявные правила бывают неожиданными, поэтому в application code лучше явно преобразовывать данные на границах.

Что такое event bubbling?

Bubbling — фаза события, когда оно идет от target вверх по ancestors до document/window. На этом основан event delegation. Обработчик должен проверять event.target, event.currentTarget и границы контейнера, чтобы не обработать чужой элемент.

Что такое event capturing?

Capturing — фаза события до target, когда событие идет сверху вниз от window/document к целевому элементу. Обработчик подключают через {capture: true}. Этот режим используют реже, например для инфраструктурного перехвата или сложных UI, где порядок bubbling недостаточен.

Что такое strict mode?

Strict mode включает более строгие правила JavaScript: запрещает часть неявных globals, меняет this в обычном вызове на undefined, делает некоторые ошибки явными и блокирует устаревшие конструкции. ES modules и class body работают в strict mode автоматически. Минус обычно только в том, что старый небрежный код начинает падать раньше.

Что такое mutable и immutable objects?

Mutable object можно изменить на месте, сохранив ту же ссылку. Immutable-подход создает новое значение при изменении. В Angular immutability помогает predictable state, signals, OnPush/change detection и простое сравнение предыдущего и нового состояния.

Что такое higher-order function?

Higher-order function принимает функцию как аргумент или возвращает функцию. Примеры: map, filter, middleware, decorators и factories. Такой подход позволяет отделять общий алгоритм от конкретного действия, но чрезмерная вложенность ухудшает читаемость.

Что такое destructuring?

Destructuring извлекает значения из массива или объекта в переменные по структуре. Он удобен для параметров функции, DTO mapping и работы с tuples. Важно помнить про defaults и то, что destructuring не делает deep clone.

const user = {name: 'Max', role: 'admin'};
const {name, role = 'user'} = user;
Что такое template literal?

Template literal — строка в backticks с interpolation через ${...} и поддержкой многострочного текста. Он удобен для сборки человекочитаемых строк, но не должен использоваться для небезопасной вставки HTML или SQL без escaping. Tagged templates позволяют библиотекам контролировать обработку частей строки.

Что такое currying?

Currying превращает функцию от нескольких аргументов в цепочку функций по одному аргументу. Это удобно для частичного применения, factories и композиции. В обычном application code его стоит использовать там, где он реально делает вызовы понятнее, а не ради функционального стиля.

Что такое Promise?

Promise представляет будущий результат asynchronous operation: pending, fulfilled или rejected. Он позволяет строить цепочки через then, обрабатывать ошибки через catch и писать последовательный async-код через async/await. Promise не отменяет операцию сам по себе; для fetch используют AbortController.

Middle #

Зачем команде нужны JavaScript principles?

JavaScript principles фиксируют базовые договоренности: как писать modules, работать с async code, обрабатывать ошибки, именовать функции, избегать side effects и организовывать shared utilities. Это уменьшает хаос в больших проектах и делает поведение кода предсказуемым для review, testing и production debugging.

Когда проекту нужны polyfills или shims?

Polyfills нужны, когда приложение использует JavaScript-возможность или Web API, которых нет в целевых браузерах. Их список должен зависеть от browser support policy и реальной аналитики, а не добавляться на всякий случай. Лишние polyfills увеличивают bundle size и могут ухудшать startup performance.

Почему third-party scripts нужно контролировать?

Third-party scripts влияют на performance, security, privacy и стабильность приложения. Analytics, maps, chats, A/B tools и widgets часто грузятся вне основного lifecycle приложения. Их нужно инвентаризировать, ограничивать, мониторить и по возможности загружать лениво или после согласия пользователя.

Зачем команде договариваться о JavaScript commenting?

Комментарии должны объяснять причину решения, ограничение, invariant или важный edge case, а не очевидный синтаксис. Если код требует много комментариев, возможно, его стоит упростить. Для публичных API, shared utilities и сложной бизнес-логики полезны JSDoc или TSDoc, если они поддерживаются командным workflow.

Что значит следовать JavaScript patterns?

Это значит использовать узнаваемые подходы к организации кода: modules, factories, observers, adapters, dependency injection, immutable updates и другие patterns. Pattern полезен, когда упрощает понимание и расширение кода. Если он добавляет только церемонию и не решает реальную проблему, maintainability ухудшается.

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

this определяется способом вызова функции: как method, через call/apply/bind, через new или как обычная функция. В strict mode обычный вызов дает undefined, а не global object. Для Angular это важно, когда class method передают callback без привязки контекста.

Как arrow function меняет поведение this?

Arrow function не имеет собственного this, arguments и prototype; она лексически берет this из внешней области. Это удобно для callbacks, где нужно сохранить context, но плохо подходит для prototype methods и constructors. В class fields arrow methods создаются на instance, а не на prototype.

Чем null, undefined и undeclared variable отличаются?

undefined означает, что значение не было задано или свойство отсутствует. null обычно явно выражает пустое значение. Undeclared variable вообще не объявлена в доступной области; чтение такой переменной вызывает ReferenceError, кроме безопасной проверки через typeof.

Какие способы итерации по массивам и объектам есть в JavaScript?

Для массивов используют for, for...of, forEach, map, filter, reduce, some, every, find и другие методы. Для объектов часто применяют Object.keys, Object.values, Object.entries или for...in с проверкой own properties. Выбор должен выражать намерение: преобразование, поиск, проверку или побочный эффект.

Чем forEach() отличается от map()?

forEach() выполняет callback ради побочного эффекта и возвращает undefined. map() возвращает новый массив той же длины с результатами преобразования. Если результат не используется, map() обычно выбран неправильно.

Когда использовать anonymous function?

Anonymous function уместна для короткого одноразового callback, когда имя не добавляет смысла. Для сложной логики, recursion, stack traces и тестирования лучше именованная функция. В review хороший критерий простой: понятно ли действие из окружающего кода без отдельного имени.

Что происходит при вызове функции с new?

new создает новый объект, связывает его prototype с Constructor.prototype, вызывает функцию с this, указывающим на новый объект, и возвращает этот объект, если constructor явно не вернул другой объект. Вызов constructor без new может сломать this или вернуть обычный результат функции.

Чем function declaration отличается от function expression?

Function declaration поднимается целиком и доступна до места объявления. Function expression создается во время выполнения выражения; если она присвоена const или let, доступ до объявления попадает в temporal dead zone. Named function expression улучшает stack traces.

Чем feature detection отличается от проверки user agent?

Feature detection проверяет наличие нужной возможности, например через CSS.supports() или 'IntersectionObserver' in window. User agent sniffing пытается угадать браузер по строке и часто ломается из-за spoofing, версий и embedded webviews. Проверка возможности обычно надежнее и лучше сочетается с progressive enhancement.

Почему == может быть опасен?

== сравнивает значения после неявного преобразования типов, что дает неожиданные результаты вроде '' == 0 и false == 0. === сравнивает без такого приведения и обычно безопаснее. Исключения должны быть осознанными и локальными, например проверка value == null для null или undefined.

Чем HTML attribute отличается от DOM property?

Attribute — текстовое значение в HTML-разметке, а DOM property — свойство объекта в памяти браузера. Некоторые значения отражаются друг в друга, но не всегда одинаково: input.value меняется при вводе, а attribute value остается начальным. В Angular property binding [value] обычно работает с DOM property, а attribute binding [attr.aria-label] пишет attribute.

Почему не стоит расширять built-in prototypes?

Добавление методов в Array.prototype или Object.prototype может конфликтовать с будущими стандартами, библиотеками и чужим кодом. Такие изменения глобальны и трудны для диагностики. Лучше использовать обычные функции, modules или локальные wrappers.

Какие плюсы и минусы у языков, которые компилируются в JavaScript?

TypeScript, Elm, ClojureScript и другие языки могут дать типы, иные модели архитектуры и tooling. Цена — build step, source maps, зависимость от компилятора и необходимость понимать итоговый JavaScript при debugging. Для Angular TypeScript является стандартным выбором, но runtime-поведение все равно определяется JavaScript и браузером.

Какие плюсы и минусы у immutability?

Плюсы: проще reasoning, undo/redo, memoization, change detection и тестирование. Минусы: дополнительные allocations, копирование больших структур и необходимость дисциплины при вложенных обновлениях. На практике immutable updates часто сочетают с нормализованными данными и точечным controlled mutation внутри локального алгоритма.

Чем synchronous и asynchronous functions отличаются?

Synchronous function выполняется до завершения в текущей task и блокирует следующий код. Asynchronous function планирует продолжение через Promise, callback, timer, event или stream, позволяя event loop выполнять другие задачи. Асинхронность не означает parallelism: CPU-heavy работа все равно может блокировать main thread.

Можно ли изменить объект, объявленный через const?

const запрещает переназначить binding, но не делает объект immutable. Свойства объекта или элементы массива можно изменить, если сам объект не заморожен и property descriptors позволяют запись. Для защиты используют Object.freeze, readonly types в TypeScript и дисциплину immutable updates.

Чем ES6 class отличается от ES5 constructor function?

ES6 class дает более строгий и читаемый синтаксис для constructor, prototype methods, inheritance, super, static members и private fields. Под капотом methods все равно живут на prototype. Class нельзя вызвать без new, а class body всегда strict mode.

Чем spread отличается от rest?

Spread раскладывает iterable или свойства объекта в месте вызова или создания значения: fn(...args), [...items]. Rest собирает оставшиеся аргументы или свойства в одну переменную: function fn(...args) или const {id, ...rest}. Одинаковый синтаксис ... означает противоположные направления данных в зависимости от контекста.

Как делить код между файлами?

В modern JavaScript используют ES modules: export и import. Module scope не загрязняет global object, а bundler может делать tree shaking и code splitting. В Angular это основа lazy routes, standalone components и разделения feature code.

Для чего нужны static class members?

Static members принадлежат class constructor, а не instance. Их используют для factories, constants, cache, utility methods и metadata, которая относится к типу в целом. Важно не превращать static state в скрытое глобальное состояние, которое мешает тестам и SSR.

Чем while отличается от do while?

while проверяет условие перед первой итерацией, поэтому тело может не выполниться ни разу. do while выполняет тело сначала и проверяет условие после, значит минимум одна итерация будет всегда. В прикладном коде важно явно показать exit condition, чтобы цикл не стал бесконечным.

Чем event.target отличается от event.currentTarget?

event.target — исходный элемент, на котором произошло событие. event.currentTarget — элемент, чей обработчик выполняется сейчас. При bubbling и event delegation эти значения часто разные.

Чем preventDefault() отличается от stopPropagation()?

preventDefault() отменяет browser default action, например отправку формы или переход по ссылке. stopPropagation() останавливает дальнейшее распространение события по DOM. Эти методы решают разные задачи и не должны использоваться автоматически в каждом обработчике.

Middle+ or Senior #

Как выбирать JavaScript framework или library для проекта?

Framework или library выбирают по задаче, экосистеме, поддержке команды, долгосрочной maintainability, performance и стоимости миграции. Хороший ответ сравнивает не только developer experience, но и риски: lock-in, bundle size, тестируемость, accessibility, SSR и доступность специалистов.

Чем call stack отличается от task queue?

Call stack хранит текущую цепочку синхронных вызовов. Task queue содержит задачи, которые event loop возьмет позже: timers, events, network callbacks. Promise callbacks попадают в microtask queue и выполняются после текущего stack перед следующей macrotask.

JavaScript coding questions #

Middle+ or Senior #

Что вернет 10 + '20' и почему?

Выражение вернет строку '1020'. Оператор + с участием строки выполняет string concatenation, поэтому число 10 приводится к строке. Для числового сложения нужно явно преобразовать строку: 10 + Number('20').

Почему 0.1 + 0.2 === 0.3 возвращает false?

Десятичные дроби 0.1 и 0.2 нельзя точно представить в binary floating point IEEE 754. Результат равен 0.30000000000000004, поэтому строгое сравнение с 0.3 ложно. Для денег используют целые minor units или decimal подход, а для display — округление.

Реализуйте add(2, 5) и add(2)(5).

Функция может проверить, передан ли второй аргумент, и вернуть либо сумму, либо функцию, ожидающую второй операнд.

function add(first, second) {
  if (second !== undefined) {
    return first + second;
  }

  return (next) => first + next;
}

Такой пример проверяет closures, arity и аккуратное отношение к falsy values.

Как развернуть строку через split, reverse, join и какие ограничения у такого подхода?

Базовый вариант:

const reversed = 'hello'.split('').reverse().join('');

Он работает для простых BMP-символов, но может ломать emoji, surrogate pairs и combining marks. Для пользовательского текста лучше учитывать Unicode grapheme clusters, например через Intl.Segmenter, если это важно для продукта.

Что делает выражение window.foo || (window.foo = 'bar')?

Если window.foo truthy, выражение вернет его значение и присваивание не выполнится из-за short-circuit. Если значение falsy, будет выполнено присваивание 'bar', и результатом станет 'bar'. Такой прием короткий, но в application code часто понятнее явный if или ??=, если нужно учитывать только null и undefined.

Почему переменная из IIFE недоступна снаружи?

IIFE создает собственную function scope, поэтому переменные внутри нее не попадают во внешнюю область. До ES modules и let/const это часто использовали для инкапсуляции и защиты от global pollution.

(function () {
  const secret = 42;
})();
Что будет с foo.length после двух push?

Если foo — массив, каждый push добавляет элемент в конец и увеличивает length на 1. Метод возвращает новую длину массива. Например, после const foo = []; foo.push(1); foo.push(2); значение foo.length будет 2.

Что произойдет в выражении foo.x = foo = {n: 2}?

Левая ссылка foo.x вычисляется до переназначения foo. Затем foo начинает указывать на новый объект {n: 2}, а свойство x записывается в старый объект, на который foo больше не указывает. Пример проверяет порядок вычисления assignment expressions и отличие ссылки на объект от переменной.

В каком порядке выведутся console.log, setTimeout и Promise.then?
console.log('one');

setTimeout(() => {
  console.log('two');
}, 0);

Promise.resolve().then(() => {
  console.log('three');
});

console.log('four');

Порядок: one, four, three, two. Сначала выполняется синхронный код, затем microtask из Promise.then, затем macrotask из setTimeout.

Чем отличаются promise chain с return, без return, с вызовом функции и с передачей функции?

Если then возвращает Promise или значение, следующий шаг ждет и получает этот результат. Если return забыли, следующий then продолжится с undefined и не дождется вложенной async-операции. then(doWork) передает функцию для будущего вызова, а then(doWork()) вызывает ее сразу и передает результат.

Почему var a = b = 3 может создать implicit global?

Выражение читается как var a = (b = 3). Если b не объявлена, в sloppy mode присваивание создаст global property, что приводит к утечке состояния. В strict mode такой код выбросит ReferenceError, поэтому переменные нужно объявлять явно.

Почему return и объект на следующей строке могут сломаться из-за ASI?

Automatic Semicolon Insertion вставит semicolon после return, если expression начинается на следующей строке. Функция вернет undefined, а object literal станет отдельным недостижимым блоком. Объект нужно писать на той же строке или оборачивать перенос правильно.

function getUser() {
  return {
    name: 'Max',
  };
}
Реализуйте duplicate([1, 2, 3]), чтобы получить [1, 2, 3, 1, 2, 3].
function duplicate(items) {
  return [...items, ...items];
}

Это shallow copy: вложенные объекты не клонируются, а остаются теми же ссылками.

Реализуйте FizzBuzz до 100.
for (let value = 1; value <= 100; value += 1) {
  const isFizz = value % 3 === 0;
  const isBuzz = value % 5 === 0;

  if (isFizz && isBuzz) {
    console.log('fizzbuzz');
  } else if (isFizz) {
    console.log('fizz');
  } else if (isBuzz) {
    console.log('buzz');
  } else {
    console.log(value);
  }
}

Задача проверяет порядок условий, modulo и читаемую структуру ветвлений.

Что вернут выражения 'hello' || 'world' и 'foo' && 'bar'?

'hello' || 'world' вернет 'hello', потому что || возвращает первое truthy значение. 'foo' && 'bar' вернет 'bar', потому что && возвращает первое falsy значение или последний operand, если все truthy. Эти операторы возвращают исходные значения, а не обязательно boolean.

Напишите IIFE и объясните, зачем она раньше использовалась.
(function () {
  const localValue = 'hidden';

  window.appName = 'Demo';
})();

IIFE выполняется сразу и создает отдельную scope для локальных переменных. До ES modules ее часто использовали, чтобы не загрязнять global scope и скрывать implementation details.

Практика по JavaScript #

Junior #

Реализуйте chunk(array, size).

Что проверяет: работу с массивами, границы цикла, edge cases.

Функция должна разбить массив на группы длиной size. Последняя группа может быть короче.

function chunk(items, size) {
  if (size <= 0) {
    throw new Error('Size must be positive');
  }

  const result = [];

  for (let index = 0; index < items.length; index += size) {
    result.push(items.slice(index, index + size));
  }

  return result;
}

chunk([1, 2, 3, 4, 5], 2); // [[1, 2], [3, 4], [5]]

На интервью важно обсудить пустой массив, size === 1, некорректный size и то, что slice делает shallow copy.

Middle #

Реализуйте retry для асинхронной операции.

Что проверяет: Promise, async/await, обработку ошибок, backoff и отмену.

async function retry(operation, options) {
  const {attempts, delayMs} = options;
  let attempt = 0;

  while (attempt < attempts) {
    try {
      return await operation();
    } catch (error) {
      attempt += 1;

      if (attempt >= attempts) {
        throw error;
      }

      await new Promise((resolve) => setTimeout(resolve, delayMs * attempt));
    }
  }
}

В production-версии стоит добавить AbortSignal, jitter, список retryable ошибок и лимит общего времени. Не все ошибки нужно повторять: 400 обычно не retryable, а 429, 502, 503 могут быть retryable при корректном backoff.

Follow-up вопросы:

  • Чем fixed delay отличается от exponential backoff?
  • Как избежать thundering herd?
  • Как сделать retry отменяемым?
Реализуйте простой EventEmitter.

Что проверяет: структуры данных, callbacks, cleanup подписок.

class EventEmitter {
  #listeners = new Map();

  on(eventName, listener) {
    const listeners = this.#listeners.get(eventName) ?? new Set();
    listeners.add(listener);
    this.#listeners.set(eventName, listeners);

    return () => {
      listeners.delete(listener);
    };
  }

  emit(eventName, payload) {
    const listeners = this.#listeners.get(eventName);

    if (!listeners) {
      return;
    }

    for (const listener of [...listeners]) {
      listener(payload);
    }
  }
}

Копия Set при emit защищает от неожиданного изменения коллекции во время обхода. На интервью можно усложнить задачу: once, wildcard events, порядок выполнения, обработка ошибок listener и удаление пустых наборов.

Реализуйте безопасное чтение значения по строковому пути.

Что проверяет: работу с объектами, optional access, защиту от некорректного пути.

function getByPath(source, path, fallback) {
  const parts = path.split('.');
  let current = source;

  for (const part of parts) {
    const canReadProperty = current !== null && typeof current === 'object' && Object.hasOwn(current, part);

    if (!canReadProperty) {
      return fallback;
    }

    current = current[part];
  }

  return current;
}

getByPath({user: {name: 'Ada'}}, 'user.name', ''); // 'Ada'

В ответе важно не использовать небезопасный eval. Для записи по пути отдельно обсуждают prototype pollution и запрет ключей вроде __proto__, constructor, prototype.

Middle+ or Senior #

Как реализовать polyfill для Promise.all?

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

function promiseAll(values) {
  return new Promise((resolve, reject) => {
    const results = [];
    let completed = 0;

    if (values.length === 0) {
      resolve([]);
      return;
    }

    values.forEach((value, index) => {
      Promise.resolve(value)
        .then((result) => {
          results[index] = result;
          completed += 1;

          if (completed === values.length) {
            resolve(results);
          }
        })
        .catch(reject);
    });
  });
}

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