Skip to main content

Вопросы по JavaScript. Версия 2

WIP

Почему значение атрибута id элемента HTML должно быть уникальным?

Потому что любой HTML-элемент с атрибутом id становится свойством глобального объекта window. Это, с одной стороны, приводит к загрязнению глобального пространства имен, что плохо, с другой - к возможности получать доступ к таким элементам в скрипте напрямую, т.е. без предварительного получения ссылки на элемент с помощью таких методов как querySelector, что хорошо:

<div id="box"></div>
console.log(box === window.box) // true

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

В чем разница между методами preventDefault и stopPropagation?

Метод preventDefault отключает стандартную обработку события браузером, а метод stopPropagation - распространение события, т.е. запуск родительских по отношению к элементу, в котором возникло событие, обработчиков.

Например, "дефолтная" обработка события submit элемента form предполагает отправку данных формы и перезагрузку страницы.

<form id="formEl">
<input type="text" id="inputEl" />
<button>Отправить</button>
</form>
formEl.onsubmit = (e) => {
e.preventDefault()
alert(inputEl.value || 'hello world')
}

Событие поднимается от целевого элемента до window через всех его предков. Элементы, через которые проходит событие (путь события), содержится в свойстве event.path.

<div id="outerBox">
<div id="innerBox">
<button id="firstButtonEl">Сначала нажми на меня</button>
<button id="secondButtonEl">А потом на меня</button>
</div>
</div>
window.onclick = (e) => {
console.log(`Событие #${e.eventNum} достигло объекта "window"`)
}

outerBox.onclick = (e) => {
console.log(`Событие #${e.eventNum} достигло внешнего контейнера`)
}

innerBox.onclick = (e) => {
console.log(`Событие #${e.eventNum} достигло внутреннего контейнера`)
}

firstButtonEl.onclick = (e) => {
console.log('Возникло событие нажатия первой кнопки')
e.eventNum = '1'
}

secondButtonEl.onclick = (e) => {
// !
e.stopPropagation()
console.log('Возникло событие нажатия второй кнопки')
e.eventNum = '2'
}

firstButtonEl.click()
/*
Возникло событие нажатия первой кнопки
Событие #1 достигло внутреннего контейнера
Событие #1 достигло внешнего контейнера
Событие #1 достигло объекта "window"
*/

secondButtonEl.click()
// Возникло событие нажатия второй кнопки

В чем разница между методами stopPropagation и stopImmediatePropagation?

Метод stopPropagation отключает только родительские обработчики события, а метод stopImmediatePropagation - все обработчики, кроме текущего.

<div id="outerBox">
<div id="innerBox">
<button id="firstButtonEl">Сначала нажми на меня</button>
<button id="secondButtonEl">А потом на меня</button>
</div>
</div>
outerBox.onclick = (e) => {
console.log(`Событие достигло внешнего контейнера`)
}

innerBox.onclick = (e) => {
console.log(`Событие достигло внутреннего контейнера`)
}

firstButtonEl.addEventListener('click', (e) => {
e.stopPropagation()
console.log('Первый обработчик нажатия первой кнопки')
})

firstButtonEl.addEventListener('click', (e) => {
console.log('Второй обработчик нажатия первой кнопки')
})

secondButtonEl.addEventListener('click', (e) => {
// !
e.stopImmediatePropagation()
console.log('Первый обработчик нажатия второй кнопки')
})

secondButtonEl.addEventListener('click', (e) => {
console.log('Второй обработчик нажатия второй кнопки')
})

firstButtonEl.click()
/*
Первый обработчик нажатия первой кнопки
Второй обработчик нажатия первой кнопки
*/

secondButtonEl.click()
// Первый обработчик нажатия второй кнопки

В чем разница между target и currentTarget?

Свойство target содержит ссылку на элемент, в котором возникло событие (инициатор события), а currentTarget - ссылку на элемент, в котором событие обрабатывается в данный момент.

<div id="divEl">
<button id="buttonEl">Нажми на меня</button>
</div>
buttonEl.onclick = (e) => {
console.log(`
Событие возникло в "${e.target.id}".
В данный момент событие обрабатывается в "${e.currentTarget.id}".
`)
}

divEl.onclick = (e) => {
console.log(`
Событие возникло в "${e.target.id}".
В данный момент событие обрабатывается в "${e.currentTarget.id}".
`)
}

buttonEl.click()
/*
Событие возникло в "buttonEl".
В данный момент событие обрабатывается в "buttonEl".

Событие возникло в "buttonEl".
В данный момент событие обрабатывается в "divEl".
*/

В чем разница между target и relatedTarget?

Свойство relatedTarget содержит доступную только для чтения ссылку на производный целевой элемент (secondary target) событий мыши или установки фокуса, если таковой имеется. Значение данного свойства зависит от типа события.

Для события мыши:

Тип событияtargetrelatedTarget
mouseenter, mouseover или dragenterэлемент, над которым курсор находится в данный моментэлемент, над которым курсор находился ранее
mouseleave, mouseout или dragleaveэлемент, над которым курсор находился ранееэлемент, над которым курсор находится в данный момент

Для события установки фокуса:

Тип событияtargetrelatedTarget
blur или focusoutэлемент, потерявший фокусэлемент, получивший фокус
focus или focusinэлемент, получивший фокусэлемент, потерявший фокус

Пример для события мыши:

<div id="greenBox"></div>
<div id="blueBox"></div>
<p id="enterLog"></p>
<p id="leaveLog"></p>
div {
width: 100px;
height: 100px;
}
div:first-of-type {
background-color: green;
}
div:last-of-type {
background-color: blue;
}
greenBox.addEventListener('mouseenter', onMouseEnter)
greenBox.addEventListener('mouseleave', onMouseLeave)

blueBox.addEventListener('mouseenter', onMouseEnter)
blueBox.addEventListener('mouseleave', onMouseLeave)

const getRelatedTarget = (e) =>
!e.relatedTarget
? 'unknown'
: e.relatedTarget.id
? e.relatedTarget.id
: e.relatedTarget.localName

function onMouseEnter(e) {
enterLog.textContent = `В "${e.target.id}" из "${getRelatedTarget(e)}"`
}

function onMouseLeave(e) {
leaveLog.textContent = `Из "${e.target.id}" в "${getRelatedTarget(e)}"`
}

Какие настройки принимает метод addEventListener?

В качестве третьего опционального параметра метод addEventListener принимает объект options со следующими настройками:

  • capture - если имеет значение true, событие перехватывается на стадии погружения, перед обработкой в целевом элементе:
<div id="firstDivEl">
<button id="firstButtonEl">Первая кнопка</button>
</div>
<div id="secondDivEl">
<button id="secondButtonEl">Вторая кнопка</button>
</div>
function onClick(e) {
console.log(e.currentTarget.id)
}

firstDivEl.addEventListener('click', onClick)
firstButtonEl.addEventListener('click', onClick)

// !
secondDivEl.addEventListener('click', onClick, { capture: true })
secondButtonEl.addEventListener('click', onClick)

firstButtonEl.click()
/*
firstButtonEl
firstDivEl
*/

secondButtonEl.click()
/*
secondDivEl
secondButtonEl
*/
  • once - если имеет значение true, обработчик запускается только один раз. В этом случае обработчик автоматически удаляется после вызова:
<button id="firstButtonEl">Первая кнопка</button>
<button id="secondButtonEl">Вторая кнопка</button>
function onClick(e) {
console.log(e.currentTarget.id)
}

firstButtonEl.addEventListener('click', onClick)
secondButtonEl.addEventListener('click', onClick, { once: true })

firstButtonEl.click()
firstButtonEl.click()
firstButtonEl.click()
/*
firstButtonEl
firstButtonEl
firstButtonEl
*/

secondButtonEl.click()
secondButtonEl.click()
secondButtonEl.click()
/*
secondButtonEl
*/
  • passive - данная настройка предназначена для управления блокировкой рендеринга обработкой события прокрутки, но ни один современный браузер не позволит вам этого сделать, так что просто забудьте про нее.

Вместо options методу addEventListener может быть передано логическое значение для настройки capture. Данный параметр называется useCapture.

В чем заключаются преимущества использования addEventListener перед onclick, например?

Во-первых, addEventListener в отличие от onlick позволяет добавить к элементу несколько обработчиков:

<button id="buttonEl">Click me</button>
buttonEl.onclick = () => {
console.log('hi')
}
buttonEl.onclick = () => {
console.log('bye')
}
buttonEl.click() // bye
// первый обработчик был перезаписан

buttonEl.addEventListener('click', () => {
console.log('hi')
})
buttonEl.addEventListener('click', () => {
console.log('bye')
})
buttonEl.click()
/*
hi
bye
*/

Во-вторых, синтаксис addEventListener является более декларативным, хотя и более многословным:

function sayHi() {
console.log('hi')
}
buttonEl.onclick = sayHi
buttonEl.removeEventListener('click', sayHi)
buttonEl.click() // hi
// для того, чтобы удалить обработчик, требуется перезаписать свойство `onclick`
// об этом легко забыть, в результате возникнет утечка памяти
buttonEl.onclick = null
buttonEl.click()

buttonEl.addEventListener('click', sayHi)
buttonEl.removeEventListener('click', sayHi)
buttonEl.click()