Вопросы и ответы с собеседований по JavaScript

Собрали подборку самых частых вопросов на собеседовании фронтенд-разработчика с подробными ответами. Материал охватывает основы языка, асинхронность, работу с DOM, объекты, функции и ES6+.

Если вы готовитесь к собеседованию или хотите закрепить базу — этот чит-лист для вас. А чтобы знания не оставались теорией, попробуйте решить задачи на практике на Девстанции — интерактивной платформе с паутиной задач, системой уровней и AI-ревью кода.


Содержание

  1. Что такое ECMAScript?
  2. Типы данных JavaScript
  3. Переменные: var, let, const
  4. Область видимости
  5. Поднятие переменных (Hoisting)
  6. Строгий режим
  7. Приведение типов
  8. Операторы == и ===
  9. null и undefined
  10. Что такое this?
  11. Замыкание (Closure)
  12. Прототипы
  13. Функции
  14. Классы
  15. Модули
  16. Деструктуризация
  17. Spread и Rest операторы
  18. Шаблонные литералы
  19. Map и Set
  20. WeakSet, WeakMap
  21. Promise
  22. async/await
  23. Генераторы
  24. DOM
  25. События: всплытие, погружение, делегирование
  26. Shadow DOM
  27. Хранение данных в браузере
  28. Клонирование объектов
  29. Symbol
  30. Polyfills и шимы
  31. Транспиляция
  32. Перебор массива
  33. Массив Array.prototype.\*
  34. Очистка массива
  35. Event Loop (Цикл событий)
  36. Различия ES5 и ES6
  37. 0.1 + 0.2 === 0.3
  38. Сравнение объектов
  39. Проверка свойства в объекте
  40. Проверка массива
  41. Объектная обертка (Wrapper Objects)
  42. Optional chaining и nullish coalescing
  43. Разница между slice и splice

Что такое ECMAScript?

ECMAScript — это спецификация, стандарт скриптовых языков программирования, он является основой JS, поэтому любые изменения ECMAScript отражаются на JS.

ECMAScript сам по себе — это не язык программирования. А вот JavaScript — это язык программирования, реализующий стандарт ECMAScript.


Перечислите типы данных JavaScript

  • undefined
  • null
  • boolean
  • number
  • string
  • bigint
  • object
  • symbol (ES6)

Различия между var, let и const

var

  • Поднимается в начало области видимости функции при компиляции (имеет область видимости функции)
  • Переменную можно переприсвоить (присвоить новое значение)
  • Можно объявить повторно в той же области видимости

let

  • Не поднимается в начало области видимости блока при компиляции (имеет блочную область видимости)
  • Переменную можно переприсвоить (присвоить новое значение)
  • Нельзя объявить повторно в той же области видимости

const

  • Не поднимается в начало области видимости блока при компиляции (имеет блочную область видимости)
  • Переменную нельзя переприсвоить или объявить повторно

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

Область видимости — это область, ограничивающая доступ к переменным и функциям внутри себя, как бы инкапсулирующая их. JS имеет три типа областей видимости: глобальная, функциональная и блочная (ES6).

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

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

Блочная — переменные, объявленные с помощью ключевых слов let и const, доступны только внутри блока {}, в котором были объявлены.

Область видимости — это также набор правил, по которым осуществляется поиск переменной. Если переменной не существует в текущей области видимости, её поиск производится выше, во внешней по отношению к текущей области видимости. Если и во внешней области видимости переменная отсутствует, её поиск продолжается вплоть до глобальной области видимости. Если в глобальной области видимости переменная обнаружена, поиск прекращается, если нет — выбрасывается исключение. Поиск осуществляется по ближайшим к текущей областям видимости и останавливается с нахождением переменной. Это называется цепочкой областей видимости (Scope Chain).


Что такое поднятие переменных (Hoisting)?

Поднятие (hoisting) — это термин, описывающий подъем переменной или функции в глобальную или функциональную область видимости.

Контекст выполнения имеет две фазы — компиляция и выполнение.

Компиляция. В этой фазе объявления функций (function declarations) и переменные, объявленные с помощью ключевого слова var, поднимаются в самый верх глобальной (или функциональной) области видимости — как бы перемещаются в начало нашего кода. Переменные var при этом получают значение undefined, а функции — своё тело. Это объясняет, почему мы можем вызывать функции до их объявления.

Выполнение. В этой фазе переменным присваиваются значения, а функции вызываются и выполняются.

Запомните:

  • Объявления функций (function foo() {}) поднимаются целиком — и имя, и тело.
  • Переменные var поднимаются, но с значением undefined до момента присваивания.
  • let и const технически поднимаются, но остаются в так называемой зоне видимости времён (Temporal Dead Zone, TDZ) до момента объявления. Обращение к ним до объявления вызывает ошибку ReferenceError.
  • Стрелочные функции и функциональные выражения (const foo = () => {}) не поднимаются — только присваивание переменной.

Что такое строгий режим ("use strict")?

«use strict» — это директива ES5, которая заставляет весь наш код или код отдельной функции выполняться в строгом режиме. Строгий режим вводит некоторые ограничения по написанию кода, тем самым позволяя избегать ошибок на ранних этапах.

Ограничения строгого режима:

  1. Нельзя присваивать значения или обращаться к необъявленным переменным
  2. Запрещено присваивать значения глобальным переменным, доступным только для чтения или записи
  3. Нельзя удалить «неудаляемое» свойство объекта
  4. Запрещено дублирование параметров функции
  5. Нельзя создавать функции с помощью функции eval
  6. Значением this по умолчанию является undefined

Что такое приведение типов?

Конвертация встроенных типов данных друг в друга в JavaScript называется приведением типов (coercion). Оно бывает явное (explicit) и неявное (implicit).

Пример явного приведения:

let a = "42"; // строка
let b = Number(a);

a; // "42" - строка
b; // 42 - число

Пример неявного приведения:

let a = "42"; // строка
let b = a * 1; // строка "42" неявно приводится к числу

a; // "42" - строка
b; // 42  - число

Особенно важно понимать, как происходит приведение к логическому типу, так как оно часто происходит в условиях.

Следующие значения приводятся к false:

  • "" (пустая строка)
  • 0, -0, NaN
  • null, undefined
  • false

А эти — к true:

  • «hello» (любая непустая строка, даже " " — строка с пробелом)
  • 42 (число, отличное от нуля)
  • true
  • [], [1, "2"] (любой массив, даже пустой)
  • {}, {a: 42} (любой объект, даже пустой)
  • function foo() {} (любая функция)

В чём разница между "==" и "==="?

Разница между оператором нестрогого равенства == и оператором строгого равенства === состоит в том, что первый сравнивает значения, осуществляя их приведение к одному типу (Coersion), а второй — без такого преобразования.

console.log(10 == "10"); // true
console.log(10 === "10"); // false

Оператор строгого равенства === выдаст true только в том случае, если сравниваемые сущности относятся к одному и тому же типу данных и равны по значениям.


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

undefined представляет собой значение по умолчанию:

  • переменной, которой не было присвоено значение
  • функции, которая ничего не возвращает явно, например, console.log(1)
  • несуществующего свойства объекта

null — это значение, которое присваивается переменной явно.

При сравнении null и undefined мы получаем true, когда используем оператор ==, и false при использовании оператора ===.

Почему typeof null возвращает "object"?

typeof null == 'object' всегда будет возвращать true по историческим причинам.

От сообщества поступало предложение исправить эту ошибку, изменив typeof null = 'object' на typeof null = 'null', но оно было отклонено в интересах сохранения обратной совместимости (такое изменение повлекло бы за собой большое количество ошибок).

Для проверки, является ли значение null, можно использовать оператор строгого равенства ===:

function isNull(value) {
  return value === null;
}

Какое значение у "this"?

this ссылается на значение объекта, который в данный момент выполняет функцию. Значение this определяется в момент вызова функции, а не объявления.

Правила определения this:

  1. Вызов как метод объектаthis указывает на объект:
const user = {
  name: "Alex",
  greet() {
    console.log(this.name); // 'Alex'
  },
};
user.greet(); // this === user
  1. Обычный вызов функцииthis равен undefined (strict mode) или глобальному объекту (non-strict):
function showThis() {
  console.log(this);
}
showThis(); // undefined (strict mode) или window
  1. В конструкторе классаthis указывает на создаваемый экземпляр:
class User {
  constructor(name) {
    this.name = name;
  }
}
const u = new User("Alex"); // this === u
  1. Стрелочные функции — не имеют собственного this. Наследуют this из внешнего лексического окружения:
const user = {
  name: "Alex",
  greet: () => {
    console.log(this.name); // undefined — this берётся из внешней области
  },
};
  1. call, apply, bind — явно задают this:
function greet() {
  console.log(this.name);
}
const user = { name: "Alex" };
greet.call(user); // 'Alex'
greet.apply(user); // 'Alex'
const bound = greet.bind(user);
bound(); // 'Alex'
  1. Обработчик событияthis указывает на элемент, на котором висит обработчик:
button.addEventListener("click", function () {
  console.log(this); // <button>...</button>
});
// В стрелочной функции this будет внешний объект, а не элемент
  1. В strict mode на верхнем уровнеthis равен undefined (в модулях ES6 strict mode включён по умолчанию).

Что такое замыкание?

function makeCounter(initial = 0) {
  let counter = initial;

  return function () {
    return (counter += 1);
  };
}

const counterA = makeCounter(0);

console.log(counterA()); // 1
console.log(counterA()); // 2

const counterB = makeCounter(100);

console.log(counterB()); // 101
  1. Замыкание — это способность функции во время создания запоминать ссылки на переменные и параметры, находящиеся в текущей области видимости. Обычно область видимости определяется при создании функции.
  1. Замыкание — это комбинация функции и лексического окружения, в котором она была определена. Это позволяет ей обращаться к переменным и функциям этого лексического окружения в дальнейшем.

В примере кода выше мы определяем функцию makeCounter(), которая фактически является фабричной функцией. При вызове она возвращает дочернюю функцию, которая имеет доступ к лексическому окружению внешней функции, то есть функции makeCounter(). Таким образом, возвращаемая функция запоминает переменную counter и в последующем изменяет её значение.


Что такое прототип объекта?

Прототип — это независимый объект, хранящий свойства и методы, доступные всем объектам, чей __proto__ ссылается на него (на этот прототип). На этом принципе строится прототипное наследование в JS.

const o = {};
console.log(o.toString()); // [object Object]

Несмотря на то, что объект o не имеет свойства toString, обращение к этому свойству не вызывает ошибки. Если определенного свойства нет в объекте, его поиск осуществляется сначала в прототипе объекта, затем в прототипе прототипа объекта и так до тех пор, пока свойство не будет найдено. Это называется цепочкой прототипов. На вершине цепочки прототипов находится объект, доступный по обращению Object.prototype.

console.log(o.toString === Object.prototype.toString); // true

Как создать объект без прототипа?

const o1 = {};
console.log(o1.toString); // [object Object]

const o2 = Object.create(null);

console.log(o2.toString); // o2.toString is not a function

Функции

В чём разница между обычной функцией и функциональным выражением?

hoistedFunc();
notHoistedFunc();

function hoistedFunc() {
  console.log("I am hoisted");
}

var notHoistedFunc = function () {
  console.log("I will not be hoisted!");
};

Вызов notHoistedFunc приведет к ошибке, а вызов hoistedFunc нет, потому что hoistedFunc «всплывает», поднимается в глобальную область видимости, а notHoistedFunc нет.

Что такое стрелочные функции?

(argument1, argument2, ...argumentN) => {
  // тело функции
};

Это анонимные функции с особым синтаксисом, выполняющиеся в контексте включающей их области видимости, то есть в контексте блока, в котором они объявлены.

У стрелочных функций нет доступа к объекту arguments. Также они не имеют своего this, а следовательно их нельзя использовать как конструкторы с ключевым словом new.

Что такое IIFE?

IIFE (Immediately Invoked Function Expression) — это функция, которая вызывается или выполняется сразу же после создания или объявления.

Для создания IIFE необходимо обернуть функцию в круглые скобки (оператор группировки), превратив ее в выражение, и затем вызвать ее с помощью ещё одних круглых скобок.

(function () {})()(function () {})()(function named(params) {})()(() => {})(
  function (global) {},
)(window);

const utility = (function () {
  return {
    // утилиты
  };
})();

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

Лучшее использование IIFE — это выполнение функций настройки инициализации и предотвращение конфликтов имен с другими переменными в глобальной области видимости (загрязнение глобального пространства имен).

Что такое функция обратного вызова (Callback)?

Коллбэк — это функция, которая передается в другую функцию как аргумент и выполняется после того, как закончатся какие-то другие операции.

function modifyArray(arr, callback) {
  arr.push(100);
  callback();
}

let arr = [1, 2, 3, 4, 5];

modifyArray(arr, function () {
  console.log("массив модифицирован", arr);
});

Для чего используется ключевое слово «new»?

Ключевое слово new используется в функциях-конструкторах для создания нового объекта (нового экземпляра класса).

function Employee(name, position, yearHired) {
  this.name = name;
  this.position = position;
  this.yearHired = yearHired;
}

const emp = new Employee("Marko Polo", "Software Development", 2017);

Ключевое слово new делает 4 вещи:

  1. Создает пустой объект.
  2. Привязывает к нему значение this.
  3. Функция наследует от functionName.prototype.
  4. Возвращает значение this, если не указано иное.

Что такое объект arguments?

arguments — это коллекция аргументов, передаваемых функции. Он имеет свойство length и поддерживает обращение по индексу к элементам, но не является массивом, из-за чего не имеет методов forEach, reduce, filter, map и так далее.

Преобразовать arguments в массив можно, например, с помощью Array.prototype.slice.

В стрелочных функциях объект arguments не работает.

Function.prototype.call

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

Отличие от apply состоит в том, что в call аргументы передаются через запятую.

Function.prototype.apply

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

Отличие от call состоит в том, что в apply аргументы передаются в виде массива.

Function.prototype.bind

Метод bind() создает новую функцию, для которой зафиксирован this и последовательность аргументов, предшествующих аргументам при вызове новой функции.

Удобно использовать для привязки контекста или закрепления параметров.

function fullName() {
  return "Hello, this is " + this.first + " " + this.last;
}

console.log(fullName()); // => Hello this is undefined undefined

var person = { first: "Foo", last: "Bar" };
console.log(fullName.bind(person)()); // => Hello this is Foo Bar

Что такое классы?

Классы — это относительно новый способ написания функций-конструкторов в JS. Это синтаксический сахар для функций-конструкторов. В основе классов лежат те же прототипы и прототипное наследование:

// ES5
function Person(firstName, lastName, age, address) {
  this.firstName = firstName;
  this.lastName = lastName;
  this.age = age;
  this.address = address;
}

Person.self = function () {
  return this;
};

Person.prototype.toString = function () {
  return "[object Person]";
};

Person.prototype.getFullName = function () {
  return this.firstName + " " + this.lastName;
};

// ES6
class Person {
  constructor(firstName, lastName, age, address) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.age = age;
    this.address = address;
  }

  static self() {
    return this;
  }

  toString() {
    return "[object Person]";
  }

  getFullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

Наследование:

// ES5
function Employee(firstName, lastName, age, address, jobTitle, yearStarted) {
  Person.call(this, firstName, lastName, age, address);
  this.jobTitle = jobTitle;
  this.yearStarted = yearStarted;
}

Employee.prototype = Object.create(Person.prototype);

Employee.prototype.describe = function () {
  return `I am ${this.getFullName()} and I have a position of ${this.jobTitle} and I started at ${this.yearStarted}`;
};

Employee.prototype.toString = function () {
  return "[object Employee]";
};

// ES6
class Employee extends Person {
  constructor(firstName, lastName, age, address, jobTitle, yearStarted) {
    super(firstName, lastName, age, address);
    this.jobTitle = jobTitle;
    this.yearStarted = yearStarted;
  }

  describe() {
    return `I am ${this.getFullName()} and I have a position of ${this.jobTitle} and I started at ${this.yearStarted}`;
  }

  toString() {
    return "[object Employee]";
  }
}

Что такое модули?

Модули позволяют объединять (использовать) код из разных файлов и избавляют нас от необходимости держать весь код в одном большом файле. До появления модулей в JS существовало две популярные системы модулей:

  • CommonJS — Node.js
  • AMD (Asynchronous Module Definition) — Браузеры

Синтаксис модулей простой: мы используем import для импорта функциональности или значений из другого файла или файлов и export для экспорта.

// Экспорт функциональности в другой файл (именной экспорт)
export function isNull(val) {
  return val === null;
}

export function isUndefined(val) {
  return val === undefined;
}

// Импорт функциональности
import {
  isNull,
  isUndefined,
  isNullOrUndefined as isValid,
} from "./helpers.js";

// Экспорт по умолчанию
export default class Helpers {
  static isNull(val) {
    return val === null;
  }
}

// Импорт по умолчанию
import Helpers from "./helpers.js";

Что такое деструктуризация?

Деструктуризация в JavaScript позволяет извлечь данные из массива или свойства объекта и присвоить их отдельным переменным. Деструктуризация удобна тем, что позволяет не писать лишний код для доступа к данным внутри объектов/массивов по индексам или ключам.

Деструктуризация массива:

let array = [1, 2, 3];

let [x, y, z] = array;

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

Деструктуризация объекта:

let user = {
  name: "Василий",
  lastname: "Пупкин",
  age: 30,
};

let { name, lastname, age } = user;

console.log(name); // "Василий"
console.log(lastname); // "Пупкин"
console.log(age); // 30

Что такое операторы spread и rest?

Spread оператор (оператор расширения) «берет» каждый отдельный элемент итерируемого объекта (массив) и «распаковывает» его в другой итерируемый объект (массив).

const tvSeriesOne = ["Ozark", "Fargo", "Dexter"];
const tvSeriesTwo = ["Mr. Robot", "Barry", "Suits"];

const tvSeries = [...tvSeriesOne, ...tvSeriesTwo];

console.log(tvSeries);
// [ "Ozark", "Fargo", "Dexter", "Mr. Robot", "Barry", "Suits" ]

Оператор rest ... (три точки) похож на оператор spread, но выполняет противоположную функцию.

Spread забирает каждый элемент из массива и распаковывает в новый массив. Оператор rest забирает каждый элемент из массива и создает из них новый массив.

Есть 2 типа задач, где оператор rest используется чаще всего — в функциях и в процессе деструктуризации.

function buyPizza(price, ...rest) {
  console.log(price); // 500
  console.log(rest);
  // Array(4) [ 3, 10, 6, 20 ]
}
buyPizza(500, 3, 10, 6, 20);
const pizza = ["Pepperoni", 2222, 5, 6, 10, 30, 1];
const [name, id, ...rest] = pizza;

console.log(name); // Pepperoni
console.log(id); // 2222
console.log(rest);
// [ 5, 6, 10, 30, 1 ]

Что такое шаблонные литералы?

Шаблонные литералы — относительно новый способ создания строк в JS. Шаблонные литералы создаются с помощью двойных обратных кавычек:

// ES5
var greet = "Hi I'm Mark";

// ES6
let greet = `Hi I'm Mark`;

В шаблонных литералах нам не нужно экранировать одинарные кавычки.

// ES5
var lastWords = "\n" + " I \n" + " am \n" + "Iron Man \n";

// ES6
let lastWords = `
  I
  am
  Iron Man
`;

В ES6 нам не нужно использовать управляющую последовательность "\n" для перевода строки.

// ES5
function greet(name) {
  return "Hello " + name + "!";
}

// ES6
function greet(name) {
  return `Hello ${name}!`;
}

В ES6 нам не нужно использовать конкатенацию строк для объединения текста с переменной: мы можем использовать выражение ${expr} для получения значения переменной.


Что такое Map и Set?

Что такое Map?

Map — это коллекция ключ/значение наподобие Object, но с более удобным и декларативным API и некоторыми функциональными особенностями. Например, Map позволяет использовать ключи любого типа.

Методы и свойства:

  • new Map() — создаёт коллекцию
  • Map.prototype.set(key, value) — записывает по ключу key значение value
  • Map.prototype.get(key) — возвращает значение по ключу или undefined
  • Map.prototype.has(key) — возвращает true, если ключ присутствует в коллекции
  • Map.prototype.delete(key) — удаляет элемент по ключу
  • Map.prototype.clear() — очищает коллекцию
  • Map.prototype.size — возвращает текущее количество элементов
let map = new Map();

map.set("1", "str1"); // строка в качестве ключа
map.set(1, "num1"); // цифра как ключ
map.set(true, "bool1"); // булево значение как ключ

// обычный объект Object приводит ключи к строкам?
// Map сохраняет тип ключей:
alert(map.get(1)); // "num1"
alert(map.get("1")); // "str1"

alert(map.size); // 3

Для перебора коллекции Map есть 3 метода:

  • Map.prototype.keys() — возвращает итерируемый объект по ключам
  • Map.prototype.values() — возвращает итерируемый объект по значениям
  • Map.prototype.entries() — возвращает итерируемый объект по парам [ключ, значение], используется по умолчанию в for..of

В отличие от обычных объектов, в Map перебор происходит в том же порядке, в каком происходило добавление элементов.

Что такое Set?

Set — это особый вид коллекции: «множество» значений (без ключей), где каждое значение может появляться только один раз.

Его основные методы:

  • new Set(iterable) — создаёт Set, копирует значения из итерируемого объекта
  • Set.prototype.add(value) — добавляет значение (если оно уже есть, ничего не делает)
  • Set.prototype.delete(value) — удаляет значение
  • Set.prototype.has(value) — возвращает true, если значение присутствует
  • Set.prototype.clear() — удаляет все значения
  • Set.prototype.size — возвращает количество элементов

Основная «изюминка» — при повторных вызовах set.add() с одним и тем же значением, оно добавляется в коллекцию лишь единожды.

Set имеет те же методы для перебора, что и Map: keys(), values(), entries().


В чём разница между WeakSet, WeakMap и обычными Set и Map?

WeakSet и WeakMap — это специальные структуры данных в JavaScript, которые отличаются особенностью хранения ссылок на объекты.

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

И напротив, в WeakSet и WeakMap хранятся слабые ссылки. Это означает, что если объект, на который есть ссылка в этих структурах, больше недоступен в коде (то есть нигде больше нет сильных ссылок на него), то сборщик мусора может удалить этот объект из памяти, даже если в WeakSet или WeakMap все еще есть ссылка на него. Таким образом, использование слабых ссылок позволяет не держать в памяти ненужные больше объекты и экономить память.

Кроме того, в WeakMap в качестве ключей могут использоваться только объекты, а не примитивные значения. А в WeakSet хранятся только объекты, без ключей.


Что такое Promise (Обещания)?

  1. Promise — объект JavaScript, предназначенный для обработки асинхронного кода. Промисы позволяют откладывать блоки кода до момента выполнения или отклонения действия.
  1. Promise — это специальный объект, который содержит своё состояние. Вначале pending («ожидание»), затем — одно из: fulfilled («выполнено успешно») или rejected («выполнено с ошибкой»).
const promise = new Promise(function (resolve, reject) {
  // Эта функция будет вызвана автоматически
  // В ней можно делать любые асинхронные операции,
  // А когда они завершатся — нужно вызвать одно из:
  // resolve(результат) при успешном выполнении
  // reject(ошибка) при ошибке
});

Методы Promise.\*

  • Promise.all() — возвращает промис, который выполнится тогда, когда будут выполнены все промисы, переданные в виде перечисляемого аргумента, или отклонено любое из переданных промисов.
  • Promise.allSettled() — возвращает промис, который исполняется когда все полученные промисы завершены (исполнены или отклонены), содержащий массив результатов исполнения полученных промисов.
  • Promise.any() — принимает итерируемый объект содержащий объекты промисов. Как только один из промисов выполнится успешно, метод возвратит единственный объект Promise со значением выполненного промиса. Если ни один из промисов не завершится успешно, тогда возвращённый объект будет отклонён.
  • Promise.race() — возвращает выполненный или отклонённый промис, в зависимости от того, с каким результатом завершится первый из переданных промисов, со значением или причиной отклонения этого промиса.
  • Promise.reject(reason) — возвращает объект Promise, который был отклонён по указанной причине.
  • Promise.resolve(value) — возвращает Promise выполненный с переданным значением.

Что такое async/await?

async function f() {
  let promise = new Promise((resolve, reject) => {
    setTimeout(() => resolve("готово!"), 1000);
  });

  let result = await promise; // будет ждать, пока промис не выполнится (*)

  alert(result); // "готово!"
}

f();

async/await — относительно новый способ написания асинхронного (неблокирующего) кода в JS. Он делает код более читаемым и чистым, чем промисы и функции обратного вызова.

Использование ключевого слова async перед функцией заставляет её возвращать промис.

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

Обработка ошибок с try/catch

async function fetchUser() {
  try {
    const response = await fetch("/api/user");
    const data = await response.json();
    return data;
  } catch (error) {
    console.error("Ошибка загрузки:", error.message);
  }
}

Это эквивалентно использованию .catch() на промисе, но записывается в более привычном синтаксисе:


Что такое генераторы?

Генераторы — специальный тип функций в JavaScript. Они отличаются от обычных тем, что могут приостанавливать своё выполнение, возвращать промежуточный результат и далее возобновлять его позже, в произвольный момент времени.

Для объявления генератора используется синтаксическая конструкция: function* (функция со звёздочкой). Её называют «функция-генератор» (generator function).

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

Основным методом генератора является next(). При вызове он возобновляет выполнение кода до ближайшего ключевого слова yield. По достижении yield выполнение приостанавливается, а значение — возвращается во внешний код:

function* generateSequence() {
  yield 1;
  yield 2;
  return 3;
}

let generator = generateSequence();

let one = generator.next();
alert(JSON.stringify(one)); // {value: 1, done: false}

let two = generator.next();
alert(JSON.stringify(two)); // {value: 2, done: false}

let three = generator.next();
alert(JSON.stringify(three)); // {value: 3, done: true}

Новые вызовы generator.next() больше не имеют смысла. Впрочем, если они и будут, то не вызовут ошибки, но будут возвращать один и тот же объект: {done: true}.


Что такое DOM?

DOM (Document Object Model) — это специальная древовидная структура, которая позволяет управлять HTML-разметкой из JavaScript-кода. Управление обычно состоит из добавления и удаления элементов, изменения их стилей и содержимого.

Браузер создаёт DOM при загрузке страницы, складывает его в переменную document и сообщает, что DOM создан, с помощью события DOMContentLoaded.

Основные методы работы с DOM

Поиск элементов:

// По ID
const el = document.getElementById("app");

// По CSS-селектору (первое совпадение)
const btn = document.querySelector(".button");

// По CSS-селектору (все совпадения)
const items = document.querySelectorAll(".item");

Изменение содержимого:

el.textContent = "Новый текст"; // текстовый контент
el.innerHTML = "<b>Жирный</b>"; // HTML-содержимое
el.setAttribute("id", "new-id"); // атрибуты
el.classList.add("active"); // работа с классами
el.style.color = "red"; // инлайн-стили

Создание и вставка элементов:

const div = document.createElement("div");
div.textContent = "Привет!";
document.body.appendChild(div); // в конец
parent.insertBefore(div, reference); // перед reference

Удаление элементов:

el.remove(); // современный способ
parent.removeChild(el); // старый способ

События: всплытие, погружение, делегирование

Что такое Event Propagation?

Когда какое-либо событие происходит в элементе DOM, оно на самом деле происходит не только в нем. Событие «распространяется» от объекта window до вызвавшего его элемента (event.target). При этом событие последовательно пронизывает всех предков целевого элемента.

Этот процесс называется Event Propagation (распространение события).

Event Propagation имеет три стадии:

  1. Фаза погружения — событие возникает в объекте window и опускается до цели события через всех ее предков.
  2. Целевая фаза — это когда событие достигает целевого элемента.
  3. Фаза всплытия — событие поднимается от event.target, последовательно проходит через всех его предков и достигает объекта window.

Что такое всплытие события?

Всплытие — это фаза события, когда оно переходит от элемента-инициатора (event.target) к объекту window, проходя по пути через все элементы, лежащие между ними.

Что такое погружение события?

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

Зачем нужны e.preventDefault() и e.stopPropagation()?

Метод event.preventDefault() отключает поведение элемента по умолчанию. Если использовать этот метод в элементе form, то он предотвратит отправку формы (submit). Если использовать его в contextmenu, то контекстное меню будет отключено. Данный метод часто используется в keydown для переопределения клавиатуры, например, при создании музыкального/видео плеера или текстового редактора.

Метод event.stopPropagation() отключает распространение события (его всплытие или погружение).

Что такое делегирование событий?

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

Такой подход возможен благодаря особенностям событийной модели DOM-дерева, а конкретно такой особенности, как всплытие событий.


Что такое Shadow DOM?

Shadow DOM (от англ. «shadow» — тень и «DOM» — Document Object Model) — это технология, которая позволяет создавать изолированные деревья DOM внутри основного дерева DOM веб-страницы.

Shadow DOM используется для инкапсуляции. Благодаря нему в компоненте есть собственное «теневое» DOM-дерево, к которому нельзя просто так обратиться из главного документа, у него могут быть изолированные CSS-правила и т.д.

Средствами теневого DOM устроены и стилизованы сложные браузерные элементы управления, например, <input type="range">. Браузер рисует их своими силами и по своему усмотрению. Их DOM-структура обычно нам не видна, но в инструментах разработчика можно её посмотреть. К примеру, в Chrome для этого нужно активировать пункт «Show user agent shadow DOM».

Мы не можем получить доступ к теневому DOM встроенных элементов с помощью обычных JavaScript-вызовов или с помощью селекторов. Это не просто обычные потомки, это мощное средство инкапсуляции.


Какие существуют методы для хранения данных в браузере?

Есть 3 основных метода хранения данных в браузере:

LocalStorage и SessionStorage

Используются для хранения пар ключ-значение. Данные, сохраненные в них, сохраняются после обновления страницы. При этом только LocalStorage может сохранять данные после перезапуска браузера. Оба хранилища могут использовать только строки в качестве ключей и значений, поэтому объекты необходимо преобразовать с помощью JSON.stringify().

Небольшие строки данных, которые хранятся в браузере. Cookie обычно устанавливаются веб-сервером с использованием заголовка Set-Cookie. Браузер затем автоматически добавляет их почти ко всем запросам на тот же домен с использованием заголовка Cookie. Один экземпляр cookie может содержать до 4 КБ данных. В зависимости от браузера, допускается более 20 cookie на сайт.

IndexedDB

Встроенная база данных, более мощная, чем LocalStorage. Это NoSQL-хранилище данных в формате JSON внутри браузера, где доступны несколько типов ключей, а значения могут быть практически любым. IndexedDB поддерживает асинхронный доступ, транзакции для обеспечения согласованности данных и создание индексов для эффективного поиска. Позволяет хранить больше данных, чем LocalStorage, может быть связана с Service Workers и другими технологиями, которые обеспечивают функционирование PWA в оффлайне.

Разница между sessionStorage и localStorage

  1. Объем хранимых данныхlocalStorage может хранить до 10 МБ данных, в то время как sessionStorage может хранить только до 5 МБ данных.
  2. Срок хранения данных — в localStorage данные не удаляются, когда закрывается браузер или вкладка. И напротив, данные в sessionStorage удаляются, когда закрывается вкладка или окно браузера.
  3. Доступность данных — из localStorage данные доступны в любом окне браузера, в то время как данные из sessionStorage доступны только из того же окна браузера, где они были сохранены.

Как выполняется клонирование объекта?

Если объект не содержит вложенных объектов, для клонирования можно использовать оператор расширения ... или метод Object.assign():

const obj = {
  firstName: "Василий",
  lastName: "Пупкин",
};
const copy1 = { ...obj };
console.log(copy1); // {firstName: 'Василий', lastName: 'Пупкин'}

// или

const copy2 = Object.assign({}, obj);
console.log(copy2); // {firstName: 'Василий', lastName: 'Пупкин'}

Если объект содержит вложенные объекты, нужно выполнить глубокое копирование. Рекомендуемый способ — встроенный метод structuredClone(), доступный во всех современных браузерах:

const obj = {
  data: {
    id: 1,
  },
};
const copy = structuredClone(obj);

structuredClone поддерживает большинство встроенных типов данных (Date, RegExp, Map, Set, ArrayBuffer и др.), в отличие от JSON, который теряет undefined, функции, Symbols и т.д.

Другие варианты глубокого копирования — JSON.parse(JSON.stringify(obj)) (относительно медленный, с ограничениями) и метод cloneDeep из библиотеки lodash.


Зачем нужен тип данных Symbol?

Символ (Symbol) — примитивный тип, значения которого создаются с помощью вызова функции Symbol. Каждый созданный символ уникален.

const sym = Symbol();
const symTwo = Symbol();

console.log(sym === symTwo);
// false

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

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

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

Символы активно используются внутри самого JavaScript, чтобы определять поведение объектов. Такие символы называются «хорошо известными» (well-known symbols).

Самый известный символ Symbol.iterator, который позволяет реализовать обход конструкции с помощью синтаксических конструкций for..of и спред-синтаксиса.


Что такое полифиллы и шимы (shim)?

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

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


Что такое транспиляция?

Транспиляция — преобразование кода, написанного в новом стандарте в его эквивалент в старом стиле (ES6 в ES5).

Как правило, для осуществления этой процедуры используется Babel.


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

Array.prototype.forEach

const a = ["a", "b", "c"];
a.forEach(function (entry) {
  console.log(entry);
});

Цикл for

Может применяться как с инкрементом, так и с декрементом.

for (let i = 0; i < a.length; i += 1) {
  console.log(a[i]);
}

Цикл for...in

Вопреки распространенному заблуждению цикл for...in перебирает не индексы массива, а перечисляемые (iterable) свойства объекта.

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

// a — разреженный массив
var a = [];
a[0] = "a";
a[10] = "b";
a[10000] = "c";
for (var key in a) {
  if (
    a.hasOwnProperty(key) &&
    /^0$|^[1-9]\d*$/.test(key) &&
    key <= 4294967294
  ) {
    console.log(a[key]);
  }
}

Цикл for...of

Оператор for (переменная) of (сущность) позволяет пройти в цикле по свойствам сущности. Оператор работает только с итерируемыми сущностями.

let iterable = [10, 20, 30];

for (let value of iterable) {
  value += 1;
  console.log(value);
}
// 11
// 21
// 31

Методы перебора

  • Array.prototype.every — возвращает true, если для каждого элемента массива колбек возвращает значение приводимое к true.
  • Array.prototype.some — возвращает true, если хотя бы для одного элемента массива колбек возвращает значение приводимое к true.
  • Array.prototype.filter — создает новый массив, включающий те элементы исходного массива, для которых колбек возвращает true.
  • Array.prototype.map — создает новый массив, состоящий из значений возращаемых колбеком.
  • Array.prototype.reduce — сводит массив к единственному значению, применяя колбек по очереди к каждому элементу массива.
  • Array.prototype.reduceRight — работает аналогично reduce, но перебирает элементы в обратном порядке.

Перечислите методы Array.prototype.\*

  • Array.prototype.at()
  • Array.prototype.concat()
  • Array.prototype.copyWithin()
  • Array.prototype.entries()
  • Array.prototype.every()
  • Array.prototype.fill()
  • Array.prototype.filter()
  • Array.prototype.find()
  • Array.prototype.findIndex()
  • Array.prototype.findLast()
  • Array.prototype.findLastIndex()
  • Array.prototype.flat()
  • Array.prototype.flatMap()
  • Array.prototype.forEach()
  • Array.prototype.includes()
  • Array.prototype.indexOf()
  • Array.prototype.join()
  • Array.prototype.keys()
  • Array.prototype.lastIndexOf()
  • Array.prototype.map()
  • Array.prototype.pop()
  • Array.prototype.push()
  • Array.prototype.reduce()
  • Array.prototype.reduceRight()
  • Array.prototype.reverse()
  • Array.prototype.shift()
  • Array.prototype.slice()
  • Array.prototype.some()
  • Array.prototype.sort()
  • Array.prototype.splice()
  • Array.prototype.toLocaleString()
  • Array.prototype.toString()
  • Array.prototype.unshift()
  • Array.prototype.values()

Столько хватит? Есть ещё.


Как очистить массив?

1. Присвоить переменной новый пустой массив

let arr = [1, 2, 3];
arr = [];

Этот способ можно использовать, если ваш код никак не ссылается на оригинальный массив, так как при этом создается совершенно новый объект. Если в какой-то переменной есть ссылка на arr, то она не изменится.

let arrayList = ["a", "b", "c", "d", "e", "f"];
let anotherArrayList = arrayList;

arrayList = [];
console.log(anotherArrayList); // ['a', 'b', 'c', 'd', 'e', 'f']

2. Обнулить длину массива

let arr = [1, 2, 3];
arr.length = 0;

Существующий массив очистится, так же как и все ссылки на него.

let arrayList = ["a", "b", "c", "d", "e", "f"];
let anotherArrayList = arrayList;

arrayList.length = 0;
console.log(anotherArrayList); // []

3. Вырезать лишние элементы

let arr = [1, 2, 3];
arr.splice(0, arr.length);

4. Удалять элементы по одному

let arr = [1, 2, 3];

while (arr.length) {
  arr.pop();
}

Что такое цикл событий (Event Loop)?

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

Стек вызовов — это структура данных (первым вошел, последним вышел), используемая для отслеживания порядка выполнения функций в текущем контексте (области видимости).

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

Очереди задач: microtask и macrotask

Асинхронные операции делятся на два типа очередей:

Microtask (очередь микрозадач) — выполняется после текущей синхронной функции, но до следующей macrotask:

  • Promise.then/catch/finally
  • queueMicrotask()
  • MutationObserver
  • async/await (часть после await)

Macrotask (очередь макрозадач) — выполняется после завершения текущей microtask-очереди:

  • setTimeout, setInterval
  • setImmediate (Node.js)
  • I/O (чтение файлов, сети)
  • UI-события (клики, нажатия клавиш)
  • MessageChannel

Порядок выполнения

console.log("1"); // Синхронная — сразу

setTimeout(() => console.log("2"), 0); // Macrotask

Promise.resolve().then(() => console.log("3")); // Microtask

queueMicrotask(() => console.log("4")); // Microtask

console.log("5"); // Синхронная — сразу

Вывод: 1, 5, 3, 4, 2

Правило: все синхронные операции → все microtask → одна macrotask → все microtask → следующая macrotask и так далее.


Какие различия между ES5 и ES6?

ECMAScript 5 (ES5) — 5-е издание ECMAScript, выпущенное в 2009 году. Поддерживается всеми современными браузерами полностью.

ECMAScript 6 (ECMAScript 2015, ES6) — 6-е издание ECMAScript, выпущенное в 2015 году. Поддерживается всеми современными браузерами полностью.

Несколько ключевых отличий двух стандартов:

1. Стрелочные функции и интерполяция в строках

// В новом стандарте можно сделать так:
const greetings = (name) => {
  return `hello ${name}`;
};

// и даже так:
const greetings = (name) => `hello ${name}`;

2. Ключевое слово const

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

const NAMES = [];

NAMES.push("Jim");
console.log(NAMES.length === 1); // true

NAMES = ["Steve", "John"]; // error

3. Блочная видимость

Переменные, объявленные с помощью новых ключевых слов let и const имеют блочную область видимости, то есть недоступны за пределами {}-блоков. Кроме того, они не поднимаются, как var-переменные.

4. Параметры по умолчанию

Теперь функцию можно инициализировать с дефолтным значением параметров:

function multiply(a, b = 2) {
  return a * b;
}
multiply(5); // 10

5. Классы и наследование

Новый стандарт ввел в язык поддержку привычного синтаксиса классов (class), конструкторы (constructor) и ключевое слово extends для оформления наследования.

6. Оператор for-of для перебора итерируемых объектов

7. Spread и rest операторы

const obj1 = { a: 1, b: 2 };
const obj2 = { a: 2, c: 3, d: 4 };
const obj3 = { ...obj1, ...obj2 };

8. Обещания (Promises)

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

const isGreater = (a, b) => {
  return new Promise((resolve, reject) => {
    if (a > b) {
      resolve(true);
    } else {
      reject(false);
    }
  });
};

isGreater(1, 2)
  .then((result) => {
    console.log("greater");
  })
  .catch((result) => {
    console.log("smaller");
  });

9. Модули

import myModule from "./myModule";

const myModule = {
  x: 1,
  y: () => {
    console.log("This is ES5");
  },
};
export default myModule;

Почему 0.1 + 0.2 === 0.3 — это false?

Действительно, в JavaScript 0.1 + 0.2 на самом деле равно 0.30000000000000004.

Дело в том, что все числа в языке (даже целые) представлены в формате с плавающей запятой (float). В двоичной системе счисления эти числа — бесконечные дроби. Для их хранения выделяется ограниченный объем памяти, поэтому возникают подобные неточности.


Можно ли сравнить объекты оператором сравнения?

let a = {
  a: 1,
};
let b = {
  a: 1,
};
let c = a;

console.log(a === b); // false
console.log(a === c); // true

В JS объекты и примитивы сравниваются по-разному. Примитивы сравниваются по значению. Объекты — по ссылке или адресу в памяти, где хранится переменная.

Вот почему первый console.log возвращает false, а второй — true. Переменные a и c ссылаются на один объект, а переменные a и b — на разные объекты с одинаковыми свойствами и значениями.

Как итог: сравнивая две переменных-объекта с помощью оператора сравнения результат будет равен true только в том случае, если обе переменных ссылаются на один и тот же объект.


Как проверить наличие свойства в объекте?

В JavaScript есть два основных способа проверить наличие свойства в объекте — метод hasOwnProperty и оператор in.

Метод hasOwnProperty() возвращает true, если указанное свойство является прямым свойством объекта, и false в противном случае. Этот метод не проверяет свойства в цепочке прототипов объекта.

Оператор in возвращает true, если указанное свойство существует в объекте, независимо от того, является ли оно собственным свойством или унаследовано:

const myObject = {
  test: "hello",
};

console.log("toString" in myObject); // true
console.log("test" in myObject); // true

console.log(myObject.hasOwnProperty("toString")); // false
console.log(myObject.hasOwnProperty("test")); // true

Как проверить, является ли объект массивом?

Для такой проверки можно использовать встроенный метод Array.isArray().

Он принимает объект в качестве аргумента и возвращает true, если объект является массивом, и false в противном случае:

let arr = [1, 2, 3];
console.log(Array.isArray(arr)); // true

let obj = { a: 1, b: 2 };
console.log(Array.isArray(obj)); // false

Что такое объектная обертка (Wrapper Objects)?

Примитивы строка, число и boolean имеют свойства и методы, несмотря на то, что они не являются объектами:

let name = "marko";

console.log(typeof name); // string
console.log(name.toUpperCase()); // MARKO

name — это строка (примитивный тип), у которого нет свойств и методов, но когда мы вызываем метод toUpperCase(), это приводит не к ошибке, а к «MARKO».

Причина такого поведения заключается в том, что name в момент обращения к его свойству временно преобразуется в объект. У каждого примитива кроме null и undefined есть объект-обертка. Такими объектами являются String, Number, Boolean, Symbol и BigInt. В нашем случае код принимает следующий вид:

console.log(new String(name).toUpperCase()); // MARKO

Временный объект отбрасывается по завершении работы со свойством или методом.


Что такое оператор optional chaining (?.) и nullish coalescing (??)?

Optional chaining ?.

Оператор ?. позволяет безопасно обращаться к вложенным свойствам объекта. Если какое-то свойство в цепочке равно null или undefined, выражение вернёт undefined вместо ошибки:

const user = {
  address: {
    street: "ул. Пушкина",
  },
};

// Без optional chaining
const street = user && user.address && user.address.street; // 'ул. Пушкина'
const zip = user && user.address && user.address.zip; // undefined

// С optional chaining
const street2 = user?.address?.street; // 'ул. Пушкина'
const zip2 = user?.address?.zip; // undefined

?. работает с свойствами, методами и элементами массивов:

user?.name; // обращение к свойству
user?.getName(); // вызов метода
arr?.[0]; // обращение к элементу массива

Nullish coalescing ??

Оператор ?? возвращает правый операнд, если левый равен null или undefined (но не 0, "" или false):

const name = null ?? "Гость"; // 'Гость'
const name2 = "Алекс" ?? "Гость"; // 'Алекс'

// Отличие от ||:
const count = 0 ?? 10; // 0  — 0 не null/undefined, поэтому остаётся
const count2 = 0 || 10; // 10 — || считает 0 ложным значением

Это делает ?? удобным для дефолтных значений, когда 0, "" или false — валидные данные.


В чём разница между slice и splice?

Эти методы часто путают из-за похожих названий, но работают они по-разному:

slicesplice
Изменяет исходный массивНетДа
ВозвращаетНовый массив с вырезанными элементамиМассив удалённых элементов
Синтаксисslice(start, end)splice(start, deleteCount, ...items)
const arr = [1, 2, 3, 4, 5];

// slice — не меняет исходный массив
const sliced = arr.slice(1, 3);
console.log(sliced); // [2, 3]
console.log(arr); // [1, 2, 3, 4, 5]

// splice — меняет исходный массив
const removed = arr.splice(1, 2, 6, 7);
console.log(removed); // [2, 3]
console.log(arr); // [1, 6, 7, 4, 5]

Запомните: slice — «нарезает» (без побочных эффектов), splice — «вырезает и вставляет» (мутирует массив).


Заключение

Эта подборка охватывает основные темы, которые часто встречаются на собеседованиях фронтенд-разработчиков. Но знание теории — это только половина успеха. Важно уметь применять эти знания на практике.

Попробуйте решить задачи на Девстанции — интерактивной платформе для прокачки навыков программирования с элементами RPG. Здесь вы найдёте паутину задач по JavaScript с тестами, бонусными условиями, AI-ревью кода и рейтингами. Каждая задача — это возможность проверить свои знания на практике.