20 вопросов по JavaScript для подготовки к собеседованию

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). При этом событие последовательно пронизывает (затрагивает) всех предков целевого элемента. Распространение события имеет три стадии или фазы:

  1. Фаза погружения (захвата, перехвата) — событие возникает в объекте Window и опускается до цели события через всех ее предков.
  2. Целевая фаза — это когда событие достигает целевого элемента.
  3. Фаза всплытия — событие поднимается от 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.


Алгоритм следующий:


  1. Если x и y имеют одинаковый тип, сравнение выполняется с помощью оператора "===".
  2. Если x = null и y = undefined возвращается true.
  3. Если x = undefined и y = null возвращается true.
  4. Если x = число, а y = строка, возвращается x == toNumber(y) (значение y преобразуется в число).
  5. Если x = строка, а y = число, возвращается toNumber(x) == y (значение x преобразуется в число).
  6. Если x = логическое значение, возвращается toNumber(x) == y.
  7. Если y = логическое значение, возвращается x == toNumber(y).
  8. Если x = строка, символ или число, а y = объект, возвращается x == toPrimitive(y) (значение y преобразуется в примитив).
  9. Если x = объект, а y = строка, символ или число, возвращается toPrimitive(x) == y.
  10. Возвращается 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».



Report Page