Skip to content

Latest commit

 

History

History
1563 lines (1062 loc) · 53.8 KB

File metadata and controls

1563 lines (1062 loc) · 53.8 KB

JavaScript

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

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

Примитивы — 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.

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

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

Чем отличаются 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-совместимых данных.

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

Какие типы данных есть в 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
Что такое область видимости в 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};
В чем отличие нативных (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).

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

Что такое 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.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 }
Что делает 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"]

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

Что такое 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 выражают намерение понятнее.

Чем 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)),
  );
}

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

Что такое 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. В прикладном коде разреженных массивов обычно избегают.

Какие базовые 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().

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

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

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

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

Что делает 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 и асинхронность

Чем 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-сигнала.

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

Чем 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.

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

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

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

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

URL и query params

Что такое 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, если параметра нет. Все значения хранятся как строки.

Чем 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().

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

Что такое 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 можно передать нескольким связанным операциям.

Как сделать 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

Что такое REST?

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

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

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

Event Loop

Чем отличается 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

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

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

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

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

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

Что такое 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 только по месту объявления обычной функции: важно место и форма вызова.

В чем отличие 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. Он делает создание конструкторов, методов и наследования удобнее, но в основе по-прежнему лежит прототипная модель.

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

Что такое 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.

Что такое 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.

Что такое макро и микро задачи в 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 или реактивной модели.