"Берегись! Это кросс-сообщение https://fknussel.com/blog/dom-bom-revisited »

Давайте вернемся к основам и кратко рассмотрим две важные концепции JavaScript: DOM и BOM.

ДОМ

Объектная модель документа (DOM) - это набор утилит для работы с XML-документами (и, соответственно, с HTML-документами, что нас, скорее всего, интересует).

Прежде чем использовать эти утилиты и помощники, DOM преобразует XML-файлы в древовидную структуру, то есть иерархию узлов. Это дерево представляет как содержание документа, так и отношения между узлами.

Типы узлов

Обратите внимание, как в приведенном выше примере у нас есть разные типы узлов: большинство из них помечены как Element, а другие - как Attribute или Text. DOM определяет разные типы узлов, которые реализуют разные интерфейсы. Вот список наиболее распространенных типов узлов:

  • Document, который является корневым узлом всех документов XML (HTML).
  • DocumentType, который представляет определение типа документа (DTD), используемое на странице (тег doctype)
  • Element, который представляет тег
  • Attr - пара "ключ-значение", представляющая атрибут тега.
  • Text, который представляет содержимое узла.
  • Comment, который представляет комментарии XML / HTML.

Интерфейс узла

Объект Node реализует и предоставляет следующие свойства и методы. Вы можете найти подробный список здесь, на MDN.

  • nodeName
  • nodeValue
  • nodeType
  • ownerDocument
  • firstChild
  • lastChild
  • childNodes
  • previousSibling
  • nextSibling
  • hasChildNodes()
  • attributes
  • parentNode
  • parentElement
  • appendChild(node)
  • removeChild(node)
  • replaceChild(newNode, previousNode)
  • insertBefore(newNode, previousNode)

Доступ к узлам

Самый простой способ получить узлы - это напрямую сослаться на них одним из следующих способов:

  • document.querySelector(selector): Element, где селектор - это что-то в строках #some-id, .some-classname или tag. Если есть несколько элементов, соответствующих селектору, вы получите только первый из них.
  • document.querySelectorAll(selector): NodeList
  • document.getElementById(id: string): Element
  • document.getElementsByClassName(classname: string): HTMLCollection
  • document.getElementsByTagName(tag: string): HTMLCollection
  • document.getElementsByName(name: string): NodeList

Мы также можем использовать большинство этих методов (кроме getElementById и getElementByName) на любом узле типа Element, который действует как корневой элемент для нашего запроса:

const wrapper = document.querySelector('#wrapper');
wrapper.getElementsByTagName('p');
wrapper.getElementsByClassName('active');
wrapper.getElementsByName('something');

Видите, как одни методы возвращают NodeList, тогда как другие возвращают HTMLCollection? HTMLCollection всегда активен, что означает, что изменения в DOM отражаются в коллекции. NodeList, с другой стороны, может быть как живым, так и статичным. document.querySelectorAll возвращает статический NodeList, что означает, что любые последующие изменения в DOM не будут отражены в статическом NodeList объекте.

Замечание о производительности. В некоторых браузерах использование querySelectorAll вместо, например, getElementsByTagName требует значительных затрат на производительность. Это связано с тем, что статический NodeList должен иметь все необходимые данные заранее, тогда как живой NodeList может быть сгенерирован и возвращен намного быстрее.

Продолжаем ... Мы также можем прибегнуть к большинству свойств интерфейса Node для обхода объекта document, указав путь до узла, который мы пытаемся захватить. Например:

document.documentElement.lastChild;
document.documentElement.childNodes[0];
document.documentElement.firstChild.nextSibling;
document.body.previousSibling;
document.body.parentNode;

Дополнительное примечание: в большинстве случаев parentNode и parentElement возвращают одно и то же. Они различаются только в том случае, если родительский узел не относится к типу Element, и в этом случае parentElement вернет null. Например:

// both return the html element
document.body.parentNode === document.body.parentElement; // true
document.documentElement.parentNode; // returns the document node
document.documentElement.parentElement; // returns null

Еще одно небольшое примечание, на этот раз о разнице между document и document.documentElement. Последний возвращает элемент html, который, скорее всего, является тем, что вам нужно, если вы пытаетесь пройти через DOM. Также им назначены разные типы узлов:

document.nodeType; // 9 = DOCUMENT_NODE
document.documentElement.nodeType; // 1 = ELEMENT_NODE

Атрибуты

Узлы DOM типа Element предоставляют свойство attributes типа NamedNodeMap, которое представляет собой набор всех атрибутов для определенного элемента.

const element = document.querySelector('#title');
const className = element.attributes.class; // dot notation
const firstAttribute = element.attributes[0]; // array notation

Обратите внимание, что любой элемент в коллекции attributes является объектом типа Node, для которого nodeType = 2, т.е.: ATTRIBUTE_NODE.

element.attributes.class.nodeType; // returns 2 = ATTRIBUTE_NODE

Мы можем получать, устанавливать и удалять атрибуты, используя следующие методы:

  • element.getAttribute('class')
  • element.setAttribute('class', 'new-classname');
  • element.setAttributeNode(attributeNode);
  • element.removeAttribute('class');

Спецификация DOM HTML позволяет получать доступ и изменять все атрибуты напрямую путем назначения новых значений:

<img src="image.jpg" alt="This is an image" class="logo logo-sm" />
// -----
const image = document.querySelector('.logo');
image.src; // returns "image.jpg"
image.alt = "This is an AWESOME image"; // updates the alt text

Это верно для всех атрибутов, кроме class. Это потому, что class - зарезервированное ключевое слово, используемое с ES6. Вот почему нам нужно вместо этого использовать альтернативное имя атрибута className. То же самое относится, например, к атрибуту for в элементе label: вместо этого мы используем htmlFor.

image.className; // returns "logo logo-sm"
image.class; // returns undefined

Обратите внимание, что для классов CSS у нас также есть доступ к свойству classList, которое представляет собой коллекцию типа DOMTokenList, в которой перечислены все имена классов элементов. Этот интерфейс реализует четыре важных метода: add, remove, toggle и contains.

image.classList; // returns ["logo", "logo-sm"]
image.classList.add('logo-awesome');
image.classList.remove('logo-sm');
image.classList.toggle('active');
image.classList.contains('this-class-doesnt-exist');
image.classList; // now returns ["logo", "logo-awesome", "active"]

Еще одна вещь, на этот раз о перечислении всех вычисленных свойств CSS элемента. Существует метод под названием window.getComputedStyles(element), который вы передаете в свой элемент и возвращает массивный объект типа CSSStyleDeclaration, который можно использовать для определения вычисленных стилей элемента, независимо от того, как они были применены.

getComputedStyle(image).width; // returns the actual image width

Создание, удаление и замена узлов

К объекту document прикреплены различные фабричные методы, которые мы можем использовать для создания узлов разных типов. Самые распространенные из них:

  • document.createElement(tag: string): Element
  • document.createAttribute(name: string): Attr
  • document.createTextNode(text: string): Text
  • document.createComment(comment: string): Comment

Некоторые примеры:

const p = document.createElement('p');
const blurb = document.createTextNode('sup mate');
const attr = document.createAttributeNode('class');
p.appendChild(blurb);
attr.value = 'my-custom-class';
p.setAttributeNode(attr);
p.setAttribute('is-awesome', true);

Чтобы удалить узлы, используйте метод removeChild, сначала запросив родительский элемент. removeChild принимает в качестве параметра элемент, который мы пытаемся удалить.

Также есть метод remove, который мы можем использовать для элементов, хотя он не поддерживается в Internet Explorer и требует полифиллинга.

const element = document.querySelector('#title');
element.parentNode.removeChild(element);
element.remove(); // this doesn't work on IE!

Мы можем заменить узел с помощью метода replaceChild. Это вроде того же процесса, который мы использовали бы для удаления узла: сначала мы запрашиваем родительский узел, затем мы вызываем метод replaceChild для родительского, передавая сначала newElement, а oldElement (тот, который мы пытаемся заменить) в качестве второй параметр, и мы вернем замененный узел.

const oldElement = document.querySelector('#title');
const newElement = document.createTextNode('this is our new title');
const newElementText = document.createTextNode('our new title');
newElement.appendChild(newElementText);
const replacedNode = oldElement.parentNode.replaceChild(newElement, oldElement);
replacedNode === oldElement; // true

Узловые отношения

Есть несколько способов установить связь между двумя узлами:

  • element.append
  • element.appendChild
  • element.insertBefore
  • element.insertAdjacentElement

События

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

  • Один элемент может прослушивать несколько типов событий и реагировать на них.
  • Один тип события может обрабатываться несколькими элементами.

Вы можете привязать обработчики событий к элементам с помощью метода addEventListener. Первый аргумент - это имя события, второй - обработчик события:

const button = document.querySelector('#button');
button.addEventListener('click', handleClick);
function handleClick(event) {
  console.log('clicked');
}

Наша функция-обработчик событий получает объект event, переданный в качестве параметра.

Чтобы прекратить прослушивание события для определенного элемента, используйте для него метод removeEventListener:

button.removeEventListener('click', handleClick);

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

Вот список наиболее широко используемых типов событий:

  • События мыши: click, dblclick, mousedown, mouseout, mouseover, mouseup, mousemove.
  • События клавиатуры: keydown, keypress, keyup.
  • События HTML: load, unload, abort, error, resize, change, submit, reset, scroll, focus, blur.
  • События DOM: DOMSubtreeModified, DOMNodeInserted, DOMNodeRemoved.

Более подробный список типов событий можно найти по этой ссылке.

Спецификация

Объектная модель браузера (BOM) позволяет JavaScript общаться с браузером по вопросам, отличным от содержимого страницы.

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

Глобальные переменные и методы

Объект window - это самый корневой элемент, все остальное прямо или косвенно привязано к нему. Следовательно, нет необходимости явно ссылаться на него.

В браузерах с вкладками каждая вкладка имеет свой собственный объект window, т.е.: он не используется вкладками в одном окне.

Кроме того, все переменные и функции, которые не связаны ни с каким другим объектом, присоединяются к объекту window и, таким образом, находятся в глобальной области видимости. Другими словами: глобальные переменные становятся свойствами объекта window, а глобальные функции становятся методами объекта window.

Глобальный объект window обеспечивает доступ к:

  • свойства, которые предоставляют информацию в окне браузера:
// outer dimensions
const outerHeight = window.outerHeight;
const outerWidth = window.outerWidth;
// inner dimensions
const innerHeight = window.innerHeight;
const innerWidth = window.innerWidth;
  • методы установки таймеров и многократного вызова функции:
const timeout = setTimeout(callback, delay); // delay in ms
const interval = setInterval(callback, delay); // delay in ms
clearTimeout(timeout);
clearInterval(interval);

И многое другое. Вот полный список всех свойств и методов глобального объекта window.

Местоположение объекта

Из-за отсутствия стандарта у нас есть объект местоположения, прикрепленный к document и window:

document.location === window.location; // returns true

Эти объекты позволяют читать и управлять URL-адресом в адресной строке браузера. Вот список всех основных свойств и методов, которые он предоставляет (более обширный список можно найти здесь):

  • location.hash
  • location.host
  • location.hostname
  • location.href возвращает полный URL-адрес текущей страницы. Мы также можем писать в это свойство, вызывая перенаправление на новое значение.
  • location.pathname возвращает все, что идет после имени хоста.
  • location.port возвращает номер порта, но только если он указан в URL.
  • location.protocol возвращает протокол, используемый для доступа к странице.
  • location.search возвращает все, что идет после ? в URL-адресе.
  • location.assign(url) переходит к URL-адресу, переданному в качестве параметра.
  • location.replace(url) аналогичен назначению, но замененный сайт удаляется из истории сеансов.
  • location.reload(), который имеет тот же эффект, что и нажатие кнопки перезагрузки в нашем браузере.

Подробнее об интерфейсе Location на MDN.

Объект истории

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

  • history.length возвращает целое число, представляющее количество элементов в истории сеанса, включая текущую загруженную страницу.
  • history.go(integer) перенаправляет на страницу в истории сеанса, определенную по ее относительному положению по отношению к текущей странице.
  • history.back(integer) выполняет перенаправление на предыдущую страницу в истории сеанса так же, как при нажатии кнопки Назад в браузере.
  • history.forward(integer) выполняет перенаправление на следующую страницу в истории сеанса, так же как при нажатии кнопки Вперед в браузере.

Подробнее об интерфейсе History здесь.

Объект навигатора

window.navigator предоставляет информацию о состоянии и идентификаторе пользовательского агента (то есть браузера и ОС, в которых работает пользователь).

  • navigator.userAgent возвращает пользовательский агент для текущего браузера.
  • navigator.language возвращает строку, представляющую предпочтительный язык пользователя, обычно язык пользовательского интерфейса браузера.
  • navigator.languages возвращает массив языков, известных пользователю, отсортированных по предпочтениям: ["en-GB", "en-US", "en"]
  • navigator.getBattery() возвращает обещание, которое преобразовано в объект BatteryManager, предоставляющий не только информацию о батарее системы, но также некоторые события, которые вы можете обработать для отслеживания состояния батареи.
  • navigator.plugins возвращает список плагинов, установленных в браузере.
  • navigator.onLine возвращает логическое значение, указывающее, работает ли браузер в сети.
  • navigator.geolocation возвращает объект Geolocation, позволяющий получить доступ к местоположению устройства.

Есть и другие доступные свойства, на которые мы не всегда можем положиться:

  • navigator.appName должен вернуть имя браузера.
  • navigator.appVersion должен вернуть номер версии браузера.
  • navigator.plaftorm должен вернуть название платформы браузера.

На этом мы завершаем общий обзор модели DOM и BOM. Пожалуйста, обратитесь к сайту MDN для получения более подробной информации. Спасибо за прочтение!