20 вопросов по JavaScript для подготовки к собеседованию
1. В чем разница между null и undefined?
Для начала давайте поговорим о том, что у них общего.
Во-первых, они принадлежат к 7 «примитивам» (примитивным типам) JS:
let primitiveTypes = ['string', 'number', 'null', 'undefined', 'boolean', 'symbol', 'bigint']
Во-вторых, они являются ложными значениями, т.е. результатом их преобразования в логическое значение с помощью Boolean() или оператора "!!" является false:
console.log(!!null) // false console.log(!!undefined) // false console.log(Boolean(null)) // false console.log(Boolean(undefined)) // false
Ладно, теперь о различиях.
undefined («неопределенный») представляет собой значение по умолчанию:
- переменной, которой не было присвоено значения, т.е. объявленной, но не инициализированной переменной;
- функции, которая ничего не возвращает явно, например, console.log(1);
- несуществующего свойства объекта.
В указанных случаях движок JS присваивает значение undefined.
let _thisIsUndefined
const doNothing = () => {}
const someObj = {
a: 'ay',
b: 'bee',
c: 'si'
}
console.log(_thisIsUndefined) // undefined
console.log(doNothing()) // undefined
console.log(someObj['d']) // undefined
null — это «значение отсутствия значения». null — это значение, которое присваивается переменной явно. В примере ниже мы получаем null, когда метод fs.readFile отрабатывает без ошибок:
fs.readFile('path/to/file', (e, data) => {
console.log(e) // здесь мы получаем null
if(e) {
console.log(e)
}
console.log(data)
})
При сравнении null и undefined мы получаем true, когда используем оператор "==", и false при использовании оператора "===". О том, почему так происходит, см. ниже.
console.log(null == undefined) // true console.log(null === undefined) // false
2. Для чего используется оператор "&&"?
Оператор "&&" (логическое и) находит и возвращает первое ложное значение либо последний операнд, когда все значения истинные. Он использует короткое замыкание во избежание лишних затрат:
console.log(false && 1 && []) // false
console.log(' ' && true && 5) // 5
С оператором «if»:
const router: Router = Router()
router.get('/endpoint', (req: Request, res: Response) => {
let conMobile: PoolConnection
try {
// операции с базой данных
} catch (e) {
if (conMobile) {
conMobile.release()
}
}
})
То же самое с оператором "&&":
const router: Router = Router()
router.get('/endpoint', (req: Request, res: Response) => {
let conMobile: PoolConnection
try {
// операции с базой данных
} catch (e) {
conMobile && conMobile.release()
}
})
3. Для чего используется оператор "||"?
Оператор "||" (логическое или) находит и возвращает первое истинное значение. Он также использует короткое замыкание. Данный оператор использовался для присвоения параметров по умолчанию в функциях до того, как параметры по умолчанию были стандартизированы в ES6.
console.log(null || 1 || undefined) // 1
function logName(name) {
let n = name || Mark
console.log(n)
}
logName() // Mark
4. Является ли использование унарного плюса (оператор "+") самым быстрым способом преобразования строки в число?
Согласно MDN оператор "+" действительно является самым быстрым способом преобразования строки в число, поскольку он не выполняет никаких операций со значением, которое является числом.
5. Что такое DOM?
DOM или Document Object Model (объектная модель документа) — это прикладной программный интерфейс (API) для работы с HTML и XML документами. Когда браузер первый раз читает («парсит») HTML документ, он формирует большой объект, действительно большой объект, основанный на документе — DOM. DOM представляет собой древовидную структуру (дерево документа). DOM используется для взаимодействия и изменения самой структуры DOM или его отдельных элементов и узлов.
Допустим, у нас есть такой HTML:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document Object Model</title>
</head>
<body>
<div>
<p>
<span></span>
</p>
<label></label>
<input>
</div>
</body>
</html>
DOM этого HTML выглядит так:

В JS DOM представлен объектом Document. Объект Document имеет большое количество методов для работы с элементами, их созданием, модификацией, удалением и т.д.
6. Что такое распространение события (Event Propagation)?
Когда какое-либо событие происходит в элементе DOM, оно на самом деле происходит не только в нем. Событие «распространяется» от объекта Window до вызвавшего его элемента (event.target). При этом событие последовательно пронизывает (затрагивает) всех предков целевого элемента. Распространение события имеет три стадии или фазы:
- Фаза погружения (захвата, перехвата) — событие возникает в объекте Window и опускается до цели события через всех ее предков.
- Целевая фаза — это когда событие достигает целевого элемента.
- Фаза всплытия — событие поднимается от event.target, последовательно проходит через всех его предков и достигает объекта Window.

Подробнее о распространении событий можно почитать здесь и здесь.
7. Что такое всплытие события?
Когда событие происходит в элементе DOM, оно затрагивает не только этот элемент. Событие «всплывает» (подобно пузырьку воздуха в воде), переходит от элемента, вызвавшего событие (event.target), к его родителю, затем поднимается еще выше, к родителю родителя элемента, пока не достигает объекта Window.
Допустим, у нас есть такая разметка:
<div class="grandparent">
<div class="parent">
<div class="child">1</div>
</div>
</div>
И такой JS:
function addEvent(el, event, callback, isCapture = false) {
if (!el || !event || !callback || typeof callback !== 'function') return
if (typeof el === 'string') {
el = document.querySelector(el)
}
el.addEventListener(event, callback, isCapture)
}
addEvent(document, 'DOMContentLoaded', () => {
const child = document.querySelector('.child')
const parent = document.querySelector('.parent')
const grandparent = document.querySelector('.grandparent')
addEvent(child, 'click', function(e) {
console.log('child')
})
addEvent(parent, 'click', function(e) {
console.log('parent')
})
addEvent(grandparent, 'click', function(e) {
console.log('grandparent')
})
addEvent('html', 'click', function(e) {
console.log('html')
})
addEvent(document, 'click', function(e) {
console.log('document')
})
addEvent(window, 'click', function(e) {
console.log('window')
})
})
У метода addEventListener есть третий необязательный параметр — useCapture. Когда его значение равняется false (по умолчанию), событие начинается с фазы всплытия. Когда его значение равняется true, событие начинается с фазы погружения (для «прослушивателей» событий, прикрепленных к цели события, событие находится в целевой фазе, а не в фазах погружения или всплытия. События в целевой фазе инициируют все прослушиватели на элементе в том порядке, в котором они были зарегистрированы независимо от параметра useCapture — прим. пер.). Если мы кликнем по элементу child, в консоль будет выведено: child, parent, grandparent, html, document, window. Вот что такое всплытие события.
8. Что такое погружение события?
Когда событие происходит в элементе DOM, оно происходит не только в нем. В фазе погружения событие опускается от объекта Window до цели события через всех его предков.
Разметка:
<div class="grandparent">
<div class="parent">
<div class="child">1</div>
</div>
</div>
JS:
function addEvent(el, event, callback, isCapture = false) {
if (!el || !event || !callback || typeof callback !== 'function') return
if (typeof el === 'string') {
el = document.querySelector(el);
}
el.addEventListener(event, callback, isCapture)
}
addEvent(document, 'DOMContentLoaded', () => {
const child = document.querySelector('.child')
const parent = document.querySelector('.parent')
const grandparent = document.querySelector('.grandparent')
addEvent(child, 'click', function(e) {
console.log('child');
}, true)
addEvent(parent, 'click', function(e) {
console.log('parent')
}, true)
addEvent(grandparent, 'click', function(e) {
console.log('grandparent')
}, true)
addEvent('html', 'click', function(e) {
console.log('html')
}, true)
addEvent(document, 'click', function(e) {
console.log('document')
}, true)
addEvent(window, 'click', function(e) {
console.log('window')
}, true)
})
У метода addEventListener есть третий необязательный параметр — useCapture. Когда его значение равняется false (по умолчанию), событие начинается с фазы всплытия. Когда его значение равняется true, событие начинается с фазы погружения. Если мы кликнем по элементу child, то увидим в консоли следующее: window, document, html, grandparent, parent, child. Это и есть погружение события.
9. В чем разница между методами event.preventDefault() и event.stopPropagation()?
Метод event.preventDefault() отключает поведение элемента по умолчанию. Если использовать этот метод в элементе form, то он предотвратит отправку формы (submit). Если использовать его в contextmenu, то контекстное меню будет отключено (данный метод часто используется в keydown для переопределения клавиатуры, например, при создании музыкального/видео плеера или текстового редактора — прим. пер.). Метод event.stopPropagation() отключает распространение события (его всплытие или погружение).
10. Как узнать об использовании метода event.preventDefault()?
Для этого мы можем использовать свойство event.defaulPrevented, возвращающее логическое значение, служащее индикатором применения к элементу метода event.preventDefault.
11. Почему obj.someprop.x приводит к ошибке?
const obj = {}
console.log(obj.someprop.x)
Ответ очевиден: мы пытается получить доступ к свойству x свойства someprop, которое имеет значение undefined. obj.__proto__.__proto = null, поэтому возвращается undefined, а у undefined нет свойства x.
12. Что такое цель события или целевой элемент (event.target)?
Простыми словами, event.target — это элемент, в котором происходит событие, или элемент, вызвавший событие.
Имеем такую разметку:
<div onclick="clickFunc(event)" style="text-align: center; margin: 15px;
border: 1px solid red; border-radius: 3px;">
<div style="margin: 25px; border: 1px solid royalblue; border-radius: 3px;">
<div style="margin: 25px; border: 1px solid skyblue; border-radius: 3px;">
<button style="margin: 10px">
Button
</button>
</div>
</div>
</div>
И такой простенький JS:
function clickFunc(event) {
console.log(event.target)
}
Мы прикрепили «слушатель» к внешнему div. Однако если мы нажмем на кнопку, то получим в консоли разметку этой кнопки. Это позволяет сделать вывод, что элементом, вызвавшим событие, является именно кнопка, а не внешний или внутренние div.
13. Что такое текущая цель события (event.currentTarget)?
Event.currentTarget — это элемент, к которому прикреплен прослушиватель событий.
Аналогичная разметка:
<div onclick="clickFunc(event)" style="text-align: center;margin:15px;
border:1px solid red;border-radius:3px;">
<div style="margin: 25px; border:1px solid royalblue;border-radius:3px;">
<div style="margin:25px;border:1px solid skyblue;border-radius:3px;">
<button style="margin:10px">
Button
</button>
</div>
</div>
</div>
И немного видоизмененный JS:
function clickFunc(event) {
console.log(event.currentTarget)
}
Мы прикрепили слушатель к внешнему div. Куда бы мы ни кликнули, будь то кнопка или один из внутренних div, в консоли мы всегда получим разметку внешнего div. Это позволяет заключить, что event.currentTarget — это элемент, к которому прикреплен прослушиватель событий.
14. В чем разница между операторами "==" и "==="?
Разница между оператором "==" (абстрактное или нестрогое равенство) и оператором "===" (строгое равенство) состоит в том, что первый сравнивает значения после их преобразования или приведения к одному типу (Coersion), а второй — без такого преобразования.
Давайте копнем глубже. И сначала поговорим о преобразовании.
Преобразование представляет собой процесс приведения значения к другому типу или, точнее, процесс приведения сравниваемых значений к одному типу. При сравнении оператор "==" производит так называемое неявное сравнение. Оператор "==" выполняет некоторые операции перед сравнением двух значений.
Допустим, мы сравниваем x и y.
Алгоритм следующий:
- Если x и y имеют одинаковый тип, сравнение выполняется с помощью оператора "===".
- Если x = null и y = undefined возвращается true.
- Если x = undefined и y = null возвращается true.
- Если x = число, а y = строка, возвращается x == toNumber(y) (значение y преобразуется в число).
- Если x = строка, а y = число, возвращается toNumber(x) == y (значение x преобразуется в число).
- Если x = логическое значение, возвращается toNumber(x) == y.
- Если y = логическое значение, возвращается x == toNumber(y).
- Если x = строка, символ или число, а y = объект, возвращается x == toPrimitive(y) (значение y преобразуется в примитив).
- Если x = объект, а y = строка, символ или число, возвращается toPrimitive(x) == y.
- Возвращается false.
Запомните: для приведения объекта к «примитиву» метод toPrimitive сначала использует метод valueOf, затем метод toString.
Примеры:

Все примеры возвращают true.
Первый пример — первое условие алгоритма.
Второй пример — четвертое условие.
Третий — второе.
Четвертый — седьмое.
Пятый — восьмое.
И последний — десятое.

Если же мы используем оператор "===" все примеры, кроме первого, вернут false, поскольку значения в этих примерах имеют разные типы.
15. Почему результатом сравнения двух похожих объектов является false?
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» — на разные объекты с одинаковыми свойствами и значениями.
16. Для чего используется оператор "!!"?
Оператор "!!" (двойное отрицание) приводит значение справа от него к логическому значению.
console.log(!!null) // false
console.log(!!undefined) // false
console.log(!!'') // false
console.log(!!0) // false
console.log(!!NaN) // false
console.log(!!' ') // true
console.log(!!{}) // true
console.log(!![]) // true
console.log(!!1) // true
console.log(!![].length) // false
17. Как записать несколько выражений в одну строку?
Для этого мы можем использовать оператор "," (запятая). Этот оператор «двигается» слева направо и возвращает значение последнего выражения или операнда.
let x = 5
x = (x++, x = addFive(x), x *= 2, x -= 5, x += 10)
function addFive(num) {
return num + 5
}
Если мы выведем значение x в консоль, то получим 27. Сначала мы увеличиваем значение x на единицу (x = 6). Затем вызываем функцию addFive() с параметром 6, к которому прибавляем 5 (x = 11). После этого мы умножаем значение x на 2 (x = 22). Затем вычитаем 5 (x = 17). И, наконец, прибавляем 10 (x = 27).
18. Что такое поднятие (Hoisting)?
Поднятие — это термин, описывающий подъем переменной или функции в глобальную или функциональную области видимости.
Для того, чтобы понять, что такое Hoisting, необходимо разобраться с тем, что представляет собой контекст выполнения.
Контекст выполнения — это среда, в которой выполняется код. Контекст выполнения имеет две фазы — компиляция и собственно выполнение.
Компиляция. В этой фазе функциональные выражения и переменные, объявленные с помощью ключевого слова «var», со значением undefined поднимаются в самый верх глобальной (или функциональной) области видимости (как бы перемещаются в начало нашего кода. Это объясняет, почему мы можем вызывать функции до их объявления — прим. пер.).
Выполнение. В этой фазе переменным присваиваются значения, а функции (или методы объектов) вызываются или выполняются.
Запомните: поднимаются только функциональные выражения и переменные, объявленные с помощью ключевого слова «var». Обычные функции и стрелочные функции, а также переменные, объявленные с помощью ключевых слов «let» и «const» не поднимаются.
Предположим, что у нас есть такой код:
console.log(y)
y = 1
console.log(y)
console.log(greet('Mark'))
function greet(name) {
return 'Hello ' + name + '!'
}
var y
Получаем undefined, 1 и 'Hello Mark!'.
Вот как выглядит фаза компиляции:
function greet(name) {
return 'Hello ' + name + '!'
}
var y // присваивается undefined
// ожидается завершение фазы компиляции
// затем начинается фаза выполнения
/*
console.log(y)
y = 1
console.log(y)
console.log(greet('Mark'))
*/
После завершения фазы компиляции начинается фаза выполнения, когда переменным присваиваются значения и вызываются функции.
Дополнительно о Hoisting можно почитать здесь.
19. Что такое область видимости (Scope)?
Область видимости — это место, где (или откуда) мы имеем доступ к переменным или функциям. JS имеем три типа областей видимости: глобальная, функциональная и блочная (ES6).
Глобальная область видимости — переменные и функции, объявленные в глобальном пространстве имен, имеют глобальную область видимости и доступны из любого места в коде.
// глобальное пространство имен
var g = 'global'
function globalFunc() {
function innerFunc() {
console.log(g) // имеет доступ к переменной g, поскольку она является глобальной
}
innerFunc()
}
Функциональная область видимости (область видимости функции) — переменные, функции и параметры, объявленные внутри функции, доступны только внутри этой функции.
function myFavouriteFunc(a) {
if (true) {
var b = 'Hello ' + a
}
return b
}
myFavouriteFunc('World')
console.log(a) // Uncaught ReferenceError: a is not defined
console.log(b) // не выполнится
Блочная область видимости — переменные (объявленные с помощью ключевых слов «let» и «const») внутри блока ({ }), доступны только внутри него.
function testBlock() {
if (true) {
let z = 5
}
return z
}
testBlock() // Uncaught ReferenceError: z is not defined
Область видимости — это также набор правил, по которым осуществляется поиск переменной. Если переменной не существует в текущей области видимости, ее поиск производится выше, во внешней по отношению к текущей области видимости. Если и во внешней области видимости переменная отсутствует, ее поиск продолжается вплоть до глобальной области видимости. Если в глобальной области видимости переменная обнаружена, поиск прекращается, если нет — выбрасывается исключение. Поиск осуществляется по ближайшим к текущей областям видимости и останавливается с нахождением переменной. Это называется цепочкой областей видимости (Scope Chain).
// цепочка областей видимости
// внутренняя область видимости -> внешняя область видимости -> глобальная область видимости
// глобальная область видимости
var variable1 = 'Comrades'
var variable2 = 'Sayonara'
function outer() {
// внешняя область видимости
var variable1 = 'World'
function inner() {
// внутренняя область видимости
var variable2 = 'Hello'
console.log(variable2 + ' ' + variable1)
}
inner()
}
outer()
// в консоль выводится 'Hello World',
// потому что variable2 = 'Hello' и variable1 = 'World' являются ближайшими
// к внутренней области видимости переменными

20. Что такое замыкание (Closures)?
Наверное, это самый сложный вопрос из списка. Я постараюсь объяснить, как я понимаю замыкание.
По сути, замыкание — это способность функции во время создания запоминать ссылки на переменные и параметры, находящиеся в текущей области видимости, в области видимости родительской функции, в области видимости родителя родительской функции и так до глобальной области видимости с помощью цепочки областей видимости. Обычно область видимости определяется при создании функции.
Примеры — отличный способ объяснить замыкание:
// глобальная область видимости
var globalVar = 'abc'
function a() {
// область видимости функции
console.log(globalVar)
}
a() // 'abc'
// цепочка областей видимости
// область видимости функции a -> глобальная область видимости
В данном примере, когда мы объявляем функцию, глобальная область видимости является частью замыкания.

Переменная «globalVar» не имеет значения на картинке, потому что ее значение может меняться в зависимости от того, где и когда будет вызвана функция. Но в примере выше globalVar будет иметь значение «abc».
Теперь пример посложнее:
var globalVar = 'global'
var outerVar = 'outer'
function outerFunc(outerParam) {
function innerFunc(innerParam) {
console.log(globalVar, outerParam, innerParam)
}
return innerFunc
}
const x = outerFunc(outerVar)
outerVar = 'outer-2'
globalVar = 'guess'
x('inner')

В результате получаем «guess outer inner». Объяснение следующее: когда мы вызываем функцию outerFunc и присваиваем переменной «x» значение, возвращаемое функцией innerFunc, параметр «outerParam» равняется «outer». Несмотря на то, что мы присвоили переменной «outerVar» значение «outer-2», это произошло после вызова функции outerFunc, которая «успела» найти значение переменной «outerVar» в цепочке областей видимости, этим значением было «outer». Когда мы вызываем «x», которая ссылается на innerFunc, значением «innerParam» является «inner», потому что мы передаем это значение в качестве параметра при вызове «x». globalVar имеет значение «guess», потому что мы присвоили ей это значение перед вызовом «x».
Пример неправильного понимания замыкания.
const arrFunc = []
for (var i = 0; i < 5; i++) {
arrFunc.push(function() {
return i
})
}
console.log(i) // 5
for (let i = 0; i < arrFunc.length; i++) {
console.log(arrFunc[i]()) // все 5
}
Данный код работает не так, как ожидается. Объявление переменной с помощью ключевого слова «var» делает эту переменную глобальной. После добавления функций в массив «arrFunc» значением глобальной переменной «i» становится «5». Поэтому когда мы вызываем функцию, она возвращает значение глобальной переменной «i». Замыкание хранит ссылку на переменную, а не на ее значение во время создания. Эту проблему можно решить, используя IIFE или объявив переменную с помощью ключевого слова «let».