[jsexpert] Понятный JavaScript (Middle) - Part 2

DOM

DOM дерево и типы node

DOM дерево и типы node
Когда мы загружаем интернет страницу в браузере, в первую очередь браузер получает HTML код и парсит его. Парсинг – это анализ входящих данных и разбитие их на структурные элементы. Затем браузер строит, из полученного HTML кода, структуру документа и потом использует ее чтобы отобразить на экране запрашиваемую интернет страницу.
С помощью JavaScript мы можем читать и изменять структуру документа. Как только что-то меняется в этой структуре, браузер реагирует на эти изменения, перестраивает страницу и визуальное отображение документа меняется.
HTML документ можно представить, как вложенные блоки. Например, HTML документ имеет следующую разметку:

<!DOCTYPE html>
<html>
  <head>
    <title>Some site</title>
  </head>
  <body>
    <h1>Title</h1>
    <p>Some text</p>
    <p>Link is
      <a href="http://jsexpert.net">here</a>.</p>
  </body>
</html>

HTML документ выше, будет иметь такую структуру в виде блоков:


Эту структуру браузер использует для отображения документа. Каждый элемент данной структуры является объектом, с которым можно взаимодействовать. Такое отображение документа называется DOM (Document Object Model).
DOM – это отображение документа в виде множества объектов, с которым можно взаимодействовать с помощью JavaScript. Такое множество объектов называют деревом объектов или ( DOM tree ). Каждый элемент DOM дерева называется нодой (node) . Таким образом, с помощью такой структуры, можно полностью взаимодействовать с каждым элементом HTML страницы.
Упрощая, можно сказать, что каждый HTML тег имеет свой, специальный JavaScript объект, который входит в DOM дерево. Взаимодействуя с этим объектом, вы фактически можете изменять HTML код либо взаимодействовать с ним.
Доступ к DOM предоставляется с помощью глобального объекта document . Объект document репрезентирует входную точку в DOM структуру.

Каждая нода может содержать дочернюю ноду, которая в свою очередь, содержит свои дочерние ноды.
Все такие DOM ноды, как уже знаем, являются объектами. У каждого такого объекта есть свойство nodeType , которое содержит числовое значение, с помощью которого можно идентифицировать тип ноды. Всего таких типов 12, но на практике чаще всего используются 2:

  1. ELEMENT_NODE – числовое значение этого типа – 1, это тип любого тега в HTML странице (<div>, <p> и т.п.);
  2. TEXT_NODE – числовое значение – 3, текстовый элемент.

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

Для того чтобы получить конкретный DOM элемент существует множество свойств и методов.
Для получения элементов html, head и body объект document содержит соответствующие свойства:

  • • document.documentElement;
  • • document.head;
  • • document.body.
document.documentElement;
// <html>
//   <head>...</head>
//   <body>...</body>
// <html>
document.head; // <head>...</head>
document.body; // <body>...</body>

Эти свойства помогают непосредственно получить перечисленные элементы из HTML документа.
document.getElementById() – получение элемента по его атрибуту id. Поскольку, по спецификации, id во всем HTML документе должно быть уникальным, то этот метод поиска является наиболее эффективным, но это не означает, что постоянно необходимо использовать только его, поскольку каждый из перечисляемых далее методов предназначен для выполнения конкретных задач в конкретной ситуации.

<div id="wrapper">
    <span class="button"></span>
</div>

<script>
    const element = document.getElementById('wrapper');
    console.log(element);
</script>

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


Как видите, мы получили полноценный HTML элемент, а также его дочерний элемент.
document.getElementsByClassName() – метод для поиска элементов с соответствующим классом. Данный метод возвращает коллекцию найденных элементов.

<div id="wrapper">
    <span class="button"></span>
    <span class="button"></span>
    <span class="button"></span>
    <span class="button"></span>
</div>

<script>
    const classes = document.getElementsByClassName('button');
    console.log(classes);
</script>

Следует отметить, что все методы для работы с DOM, которые возвращают набор элементов, возвращают именно коллекции этих элементов, а не массивы. У этих коллекций нет свойственных массивам методов, поэтому итерироваться по ним при помощи метода forEach и т.п., не получится, для их перебора используют стандартный for.
В консоли такая коллекция выглядит так:


Как видите, это именно HTML коллекция, у которой есть свойство length.
document.getElementsByTagName() – метод для поиска элементов по тегу. Возвращает коллекцию найденных по тегу элементов.
Существуют более современные методы для поиска DOM элементов.
document.querySelectorAll() – метод для поиска DOM элементов, которые удовлетворяют переданный в метод CSS селектор.

<div id="wrapper">
    <span class="button"></span>
    <span class="button">
        <div><span>Inner text</span></div>
    </span>
    <span class="button">
        <span>Some text</span>
    </span>
    <span class="button"></span>
</div>

<script>
    let elements1 = document.querySelectorAll('#wrapper .button span');
    let elements2 = document.querySelectorAll('#wrapper .button > span');
</script>

В этот метод не передаются значения атрибутов для поиска, а прямо указываются селекторы, по которым необходимо произвести поиск.
Метод возвращает абсолютно все элементы, которые будут соответствовать поиску, в виде коллекции node элементов.
document.querySelector() – метод для поиска DOM элемента, который удовлетворит переданный в метод CSS селектор.
В отличии от метода querySelectorAll, querySelector возвращает только первый найденный в DOM дереве элемент и не важно, сколько их на самом деле находится в текущем DOM дереве.

<div id="wrapper">
    <span class="button"></span>
    <span class="button"></span>
    <span class="button"></span>
</div>

<script>
    const element = document.querySelector('.button');
    console.log(element); // <span class="button"></span>
</script>

element.closest() – метод, который выполняет поиск элемента выше по иерархии, который удовлетворяет заданный CSS селектор.
Особенностью данного метода является то, что этот метод применяется не на объекте document, а на предварительно найденном DOM элементе.

<div id="wrapper">
    <span class="button"></span>
    <span class="button"></span>
    <span class="button"></span>
    <span class="button">
        <button id="add">Add</button>
    </span>
</div>

<script>
    const element = document.getElementById('add');
    element.closest('.button');
</script>

В консоли мы увидим первый найденный DOM элемент, который удовлетворяет CSS селектор.


Все перечисленные выше методы (getElementById, getElementsByClassName, getElementsByTagName, querySelectorAll, querySelector), как и метод closest, могут применяться не только на объекте document, а и на любом ранее найденном DOM элементе.

<div id="wrapper">
    <span class="button"></span>
    <span class="input"></span>
    <span class="input"></span>
    <span class="button"></span>
</div>

<script>
    const element = document.getElementById('wrapper');
    const buttons = element.querySelectorAll('.button');
    console.log(buttons);
</script>

В результате мы получим два элемента с соответствующим классом. Поиск по классу происходит не по всему HTML документу, а только по выбранному HTML элементу.

Навигация по DOM дереву

Найти элемент в DOM дереве не всегда является окончательной целью при работе с ним. Часто, бывает необходимо найти дочерние, соседние или родительский элементы относительно какого-то элемента.
Для такой навигации по DOM дереву существуют специальные свойства для навигации.
При навигации по DOM дереву можно получить доступ как к нодам любого типа (element, text, comment и т.д.), так и исключительно к нодам с типом element, что чаще всего и бывает необходимо.
Таким образом, с начала, рассмотрим свойства, которые дают возможность получить доступ к нодам любого типа:

  • • parentNode – получить доступ к родительскому элементу;
  • • childNodes – получить все дочерние элементы в виде коллекции, включая текстовые;
  • • firstChild – получить первый дочерний элемент;
  • • lastChild – получить последний дочерний элемент;
  • • previousSibling – получить соседний элемент слева (выше по дереву);
  • • nextSibling – получить соседний элемент справа (ниже по дереву).


parentNode это элемент который является контейнером относительно элемента от которого производится поиск. childNodes дает доступ к дочерним элементам относительно элемента от которого идет поиск. Результат поиска формируется в коллекцию нод, которая не является массивом.
С помощью parentNode и childNodes можно двигаться по дереву в любом направлении, но для удобства используются и другие свойства. firstChild и lastChild дают доступ к первому и последнему элементам соответственно. Если же дочерних элементов нет, то возвращается null. previousSibling и nextSibling предоставляют доступ к соседним элементам. Соседним будет элемент с общим родителем, который находится сразу перед или после элемента от которого происходит поиск.

<div id="wrapper">
    <span class="button-1"></span>
    <span class="input-3"></span>
    <p class="text"></p>
    <span class="input-4"></span>
    <span class="button-2"></span>
</div>

<script>
    const element = document.getElementsByClassName('text');
    const parent = element[0].parentNode;

    console.log(parent); // <div id="wrapper">...</div>

    console.log(parent.childNodes); // NodeList(11)
    console.log(parent.firstChild); // #text
    console.log(parent.lastChild); // #text

    console.log(element[0].previousSibling); // #text
    console.log(element[0].nextSibling); // #text
</script>

По классу text нашли элемент. Поскольку, даже если элемент только один, getElementsByClassName возвращает коллекцию элементов, выбираем первый элемент из коллекции и относительно него ищем родительский. Родительским элементом будет элемент, который выступает контейнером для нашего ранее найденного по классу элемента.
Далее, относительно найденного родительского элемента, с помощью childNodes находим все дочерние элементы. Но как видно, вместо 5 элементов (4 span и 1 p), получаем 11. Все потому, что перенос элемента на новую строку считается текстовой нодой. По этой причине, имея 6 переносов и 5 элементов, как результат получаем 11 нод, что не совсем то, что ожидали получить.


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

  • • parentElement;
  • • children;
  • • firstElementChild;
  • • lastElementChild;
  • • previousElementSibling;
  • • nextElementSibling.

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

<div id="wrapper">
    <span class="button-1"></span>
    <span class="input-3"></span>
    <p class="text"></p>
    <span class="input-4"></span>
    <span class="button-2"></span>
</div>

<script>
    const element = document.getElementsByClassName('text');
    const parent = element[0].parentElement;

    console.log(parent); // <div id="wrapper">...</div>

    console.log(parent.children); // NodeList(5)
    console.log(parent.firstElementChild); // <span class="button-1"></span>
    console.log(parent.lastElementChild); // <span class="button-2"></span>

    console.log(element[0].previousElementSibling); // <span class="input-3"></span>
    console.log(element[0].nextElementSibling); // <span class="input-4"></span>
</script>

Как видим, с помощью этого набора свойств, получили именно интересующие нас элементы из DOM дерева.
Хотелось бы обратить внимание на дочерние элементы, которых именно столько, сколько и ожидали получить – 5.


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

<div id="wrapper">
    <span class="button-1"></span>
    <span class="input-3"></span>
    <p class="text"></p>
    <span class="input-4"></span>
    <span class="button-2"></span>
</div>

<script>
    const element = document.getElementsByClassName('text');
    const parent = element[0].parentNode;
    
    let nodeArray = [];

    for (let i = 0; i < parent.childNodes.length; i++) {
        if (parent.childNodes[i].nodeType === 1) {
            nodeArray.push(parent.childNodes[i]);            
        }
    }
    console.log(nodeArray);
    // (5) [span.button-1, span.input-3, p.text, span.input-4, span.button-2]
</script>

Учитывая, что у полученных коллекций нод есть свойство length , мы можем легко проитерироваться по данной коллекции. Проходя по каждому элементу, проверяем его тип, если этот тип ноды удовлетворяет наше условие (в данном примере проверяем или тип ноды равен 1, то есть или это нода с типом element), мы добавляем его в массив nodeArray.
В результате получаем настоящий массив со всеми необходимыми нодами типа element. У этого массива элементов уже будут все методы, которые свойственны массивам.

РАБОТА С DOM

DOM свойства и методы

Работа с атрибутами
Для работы с атрибутами существует несколько методов. Методы необходимо вызывать на элементе, с атрибутами которого необходимо выполнить операции.
Методы для работы с атрибутами:

  • • element. hasAttribute() – выполняет проверку на наличие атрибута. Возвращает true или false;
  • • element. getAttribute() – получение значения атрибута;
  • • element. setAttribute() – установка значения атрибута;
  • • element. removeAttribute() – удаление атрибута.

Каждый из методов принимает один параметр – имя атрибута в виде строки. Исключением является метод setAttribute, он принимает два параметра – имя атрибута и его значение.

<div id="main" style="color: #333333">
    <button>Button</button>
</div>

<script>
    const element = document.getElementById('main');
    
    let hasAttr = element.hasAttribute('style');
    console.log(hasAttr); // true

    if (hasAttr) {
        let button = document.querySelector('button');
        button.setAttribute('disabled', 'disabled');

        let attrValue = button.getAttribute('disabled');
        console.log(attrValue); // 'disabled'
    }

    element.removeAttribute('style');
    
    hasAttr = element.hasAttribute('style');
    console.log(hasAttr); // false
</script>

В примере выше, найдя необходимый для работы элемент, проверяем, с помощью метода hasAttribute, есть ли атрибут style. Поскольку этот атрибут существует у элемента, мы получаем true.
Далее, найдя элемент button, с помощью метода setAttribute, присваиваем ему атрибут disabled с соответствующим значением и тут же проверяем установленное значение с помощью метода getAttribute.
В конце, удаляем из найденного ранее элемента атрибут style, используя метод removeAttribute, и после проверки на наличие этого атрибута, получаем результат false.
К атрибутам элементов можно обращаться и через соответствующие свойства. Например, у элемента есть атрибут id, можно получить значение этого атрибута используя одноименное свойство – element.id. Так можно обращаться к любому атрибуту, но они не всегда работают очевидно.

<div id="main" style="color: #333333">
    <button>Button</button>
</div>

<script>
    const element = document.getElementById('main');
    
    console.log(element.style); // CSSStyleDeclaration {0: "color", alignContent: "", alignItems: "", ...}
    console.log(element.id); // 'main'
</script>

Обращаясь к атрибуту style через свойство, получили не просто его значение, а целый объект. Но обратившись к атрибуту id через свойство, как и ожидали, получили его значение.
Через такие свойства, значения можно не только получать, но и присваивать.

<div id="main" style="color: #333333">
    <button>Button</button>
</div>

<script>
    const element = document.getElementById('main');
    
    console.log(element.id); // 'main'
    element.id = 'footer';
    console.log(element.id); // 'footer'
</script>

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

Работа со стилями

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

<div id="main" style="width: 200px">
    <button>Button</button>
</div>

<script>
    const element = document.getElementById('main');
    
    console.log(element.style.width); // '200px'
    element.style.width = '150px';
    console.log(element.style.width); // '150px'

    element.style.marginTop = '15px';
    element.style['margin-bottom'] = '15px';
</script>

Следует обратить внимание, что все значения извлекаются и задаются как строки. Если CSS свойство указывается через черточку, например, margin-top, то в JavaScript, как вы уже знаете, свойство нельзя указывать через черточку, поэтому CSS свойство записывается в стиле camelCase, то есть, marginTop. Или же, можно не изменять стилистику написания CSS свойства, но тогда, его необходимо указать в квадратных скобках в виде строки.
Для получения из атрибута style не объекта, а его указанные значения в виде строки, можно воспользоваться уже известным методом getAttribute, либо использовать свойство cssText из объекта CSSStyleDeclaration.

<div id="main" style="width: 200px; margin-top: 115px;"></div>

<script>
    const element = document.getElementById('main');
    
    console.log(element.style.cssText); // 'width: 200px; margin-top: 115px;'
</script>

Все вышеперечисленные способы для работы с CSS позволяют получить только те значения, которые записаны в элемент инлайново. Получить же все свойства, которые применены к конкретному элементу, можно с помощью метода getComputedStyle() .
Данный метод принимает два параметра, первый – элемент из которого следует получить стили, второй, необязательный, – псевдо-элемент (::before, ::after), если необходимо получить его стили.

<style>
    #main {
        height: 100px;
    }
</style>

<div id="main" style="width: 200px; margin-top: 115px;">Text</div>

<script>
    const element = document.getElementById('main');
    const computedStyle = getComputedStyle(element);

    console.log(computedStyle.height); // '100px'
</script>

Работа с классами
Для работы с классами специально созданы два свойства className и classList .
className – получает список классов, которые присвоены элементу, в виде строки.

<div id="main" class="header menu"></div>

<script>
    const element = document.getElementById('main');
    console.log(element.className); // 'header menu'
</script>

classList – возвращает псевдомассив DOMTokenList состоящий из классов, которые присвоены элементу.
Вернув такой псевдомассив, на нем можно вызвать несколько специальных методов:

  • • element.classList. add() – добавление нового класса к элементу;
  • • element.classList. toggle() – если указанный класс присутствует у элемента, то он будет удален, если же такого класса нет, то он будет добавлен к элементу;
  • • element.classList. contains() – проверяет, содержит ли элемент указанный класс. Возвращает true или false;
  • • element.classList. remove() – удаляет у элемента указанный класс.
<div id="main" class="header menu"></div>

<script>
    const element = document.getElementById('main');

    const classes = element.classList;
    console.log(classes);
    // DOMTokenList(2) ["header", "menu", value: "header menu"]

    classes.add('new', 'basic');
    console.log(classes);
    // DOMTokenList(4) ["header", "menu", "new", "basic", value: "header menu new basic"]

    classes.toggle('header');
    console.log(classes);
    // DOMTokenList(3) ["menu", "new", "basic", value: "menu new basic"]
    
    classes.remove('basic');
    console.log(classes);
    // DOMTokenList(2) ["menu", "new", value: "menu new"]
</script>

Работа с HTML содержимым

Для получения HTML разметки в виде строки, существуют два свойства: innerHTML и outerHTML .
element. innerHTML – получает или устанавливает разметку дочерних элементов относительно элемента, на котором было применено свойство.
element. outerHTML – получает или устанавливает разметку элемента, на котором было применено свойство, включая его дочерние элементы.

<div id="main" class="header menu">
    <button>Button</button>
</div>

<script>
    const element = document.getElementById('main');

    // Получим только дочерний элемент
    console.log(element.innerHTML); // <button>Button</button>
    
    // Получим корневой элемент, включая дочерний
    console.log(element.outerHTML);
    // <div id="main" class="header menu">
    //     <button>Button</button>
    // </div>
</script>

При установке innerHTML просто заменяет элемент, outerHTML так же непосредственно заменяет элемент в DOM, но при попытке работать с ним дальше в JavaScript, можно увидеть, что сохраняется старое значение DOM элемента.

<div id="main" class="header menu">
    <button>Button</button>
</div>

<script>
    const element = document.getElementById('main');

    // Заменит содержимое родительского контейнера
    element.innerHTML = '<a href="#">Link</a>';

    element.nodeName; // 'DIV'
    
    // Заменит корневой элемент
    element.outerHTML = '<span>Text</span>';
    
    element.nodeName; // 'DIV'
</script>

Несмотря на то, что корневой элемент div был заменен, с помощью outerHTML, на элемент span, в коде сохраняется старый экземпляр ноды. При использовании innerHTML таких проблем не возникает.

Идентификация нод

Кроме уже рассмотренного ранее свойства nodeType , с помощью которого можно определить тип ноды, произвести «идентификацию» ноды можно еще двумя свойствами: tagName и nodeName .
element. tagName – определяет имя HTML тега. Данное свойство есть только у нод типа element.
element. nodeName – определяет имя ноды. Если применить на ноде с типом element, то значение совпадет с результатом применения tagName, но если использовать на любом другом типе нод, то получим название типа ноды (например, #text).

<div id="main" class="header menu">
    <button>Button</button>
</div>

<script>
    const element = document.getElementById('main');

    for (let i = 0; i < element.childNodes.length; i++) {
        console.log(element.childNodes[i].nodeName);
    }
    // #text
    // BUTTON
    // #text

    for (let i = 0; i < element.childNodes.length; i++) {
        console.log(element.childNodes[i].tagName);
    }
    // undefined
    // BUTTON
    // undefined
</script>

Размеры и позиционирование элемента

element. clientHeight – высота элемента в пикселях, включая внутренние отступы (padding), но не поля (border) и внешние отступы (margin).
element. clientWidth – ширина элемента в пикселях, включая внутренние отступы (padding), но не поля (border) и внешние отступы (margin).
element. offsetHeight – высота элемента в пикселях, включая внутренние отступы (padding) и поля (border), но не внешние отступы (margin).
element. offsetWidth – ширина элемента в пикселях, включая внутренние отступы (padding) и поля (border), но не внешние отступы (margin).
element. offsetTop – позиция элемента в пикселях, от верхней части, относительно ближнего родительского элемента.
element. offsetLeft – позиция элемента в пикселях, от левой части, относительно ближнего родительского элемента.
Все свойства доступны только на чтение.

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

Создание DOM элементов

document.createElement() – создает ноду типа element.
document.createTextNode() – создает ноду типа text.

const button = document.createElement('button');
const buttonText = document.createTextNode('Button');

Созданные таким способом ноды, сохраняются в переменной для дальнейшей работы с ними. Такой ноде, при необходимости, можно добавить атрибуты и вставить ее в реальное DOM дерево, чтобы она отобразилась на странице.

Вставка DOM элементов

Для вставки элементов в DOM дерево существуют два метода.
element. appendChild() – добавляет элемент как дочерний в родительский (на котором был вызван метод). Если у родительского элемента уже существуют дочерние элементы, то новый будет вставлен последним.
element.i nsertBefore() – вставляет дочерний элемент перед указанным. Метод принимает два параметра, первый – новый элемент, второй – элемент, перед которым необходимо вставить новый.

<div id="main"></div>

<script>
    const element = document.getElementById('main');

    const button = document.createElement('button');
    const buttonText = document.createTextNode('Button');
    
    button.id = 'random';
    button.disabled = 'disabled';
    button.appendChild(buttonText);

    element.appendChild(button);

    const newElement = document.createElement('div');
    const randomButton = document.getElementById('random');

    newElement.innerHTML = 'Some text';

    element.insertBefore(newElement, randomButton);
</script>

Найдя родительский элемент, создаются две ноды разных типов. Ноде типа element добавляются атрибуты id и disabled. С помощью метода appendChild в элемент button добавляется ранее созданная нода типа text. Элемент button, со всеми добавленными свойствами вставляем в DOM дерево с помощью метода appendChild.
Во второй половине примера, создается новый элемент div. Для того чтобы вставить его перед ранее созданным элементом button, мы находим его в DOM дереве. Данный элемент button, на момент его поиска, уже будет вставлен и доступен в DOM дереве. Обратите внимание, текст в ноду можно добавлять и с помощью свойства innerHTML (текстовые элементы созданные с помощью document.createElement() нельзя вставлять таким способом), указав при этом необходимый текст в виде строки. Далее в методе insertBefore указываем, что хотим вставить новый элемент (newElement) перед кнопкой (randomButton).
element. textContent – свойство, которое помогает получить или установить текстовое значение. Оно схоже с работой innerHTML с разницей, что textContent вставляет и получает только текстовое содержимое. Если попытаться вставить с помощью свойства textContent текст, в котором будут, например, HTML теги, то они будут вставлены как текст, а не интерпретируются в HTML.

<div id="main">
    <button></button>
</div>

<script>
    const element = document.querySelector('button');
    element.textContent = '<span>Button</span>';

    // Кнопка будет содержать текст <span>Button</span>
    // вместо Button
</script>

element. cloneNode() – метод для клонирования ноды. Данный метод может создать как глубокую копию, то есть родительский элемент, на котором вызван метод, и все его дочерние элементы, либо только родительский. При передаче в метод true, создается глубокая копия, а при передаче false – только родительский элемент.

<div id="main">
    <button>Button</button>
</div>

<script>
    const element = document.getElementById('main');

    const deep = element.cloneNode(true);
    const parent = element.cloneNode(false);

    console.log(deep);
    // <div id="main">
    //     <button>Button</button>
    // </div>

    console.log(parent);
    // <div id="main"></div>
</script>

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

element. removeChild() – удаляет дочерний элемент из родительского.
element. remove() – удаляет непосредственно тот элемент, на котором вызван метод.
element. replaceChild() – вставляет новый элемент вместо старого. Метод принимает два параметра. Первый это новый элемент, который необходимо вставить, второй – старый элемент, который необходимо заменить.

<div id="main">
    <button>Button</button>
    <span class="text">Some text</span>
    <div id="description"></div>
</div>

<script>
    const element = document.getElementById('main');
    const description = document.getElementById('description');
    const button = document.querySelector('button');
    const text = document.querySelector('.text');

    element.removeChild(button);
    
    text.remove();

    const main = document.createElement('main');
    element.replaceChild(main, description);
</script>

Как результат выполнения методов removeChild и replaceChild, возвращаются удаленные элементы. Другими словами, при необходимости, можно сохранить в переменную удаленный ранее элемент.

<div id="main">
    <button>Button</button>
    <div id="description"></div>
</div>

<script>
    const element = document.getElementById('main');
    const description = document.getElementById('description');
    const button = document.querySelector('button');
    let deletedButton = element.removeChild(button);
    console.log(deletedButton); // <button>Button</button>

    const main = document.createElement('main');
    let oldElement = element.replaceChild(main, description);
    console.log(oldElement); // <div id="description"></div>
</script>

ОТЛАДКА СКРИПТА

Начало работы

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

  • • Клавиатура (сочетание клавиш Ctrl + Shift + I или F12)
  • • Панель меню
  • • Контекстное меню

Открыть инструменты разработчика с помощью панели меню в Firefox открыв меню, которое находится в правой верхней части окна, пройти в меню Инструменты разработки, или Инструменты — Веб-разработка — Инструменты разработки.
Для Chrome это Дополнительные инструменты — Инструменты разработчика.
В Safari . Разработка — Показать Web Inspector. Если Вы не видите меню «Разработка», зайдите в Safari — Настройки — Дополнительно, и проверьте стоит ли галочка напротив «Показать меню разработки».

Для того чтобы открыть инструменты разработчика с помощью контекстного меню необходимо нажать правой кнопкой мыши на любом участке веб-страницы, в котором необходимо выбрать пункт Исследовать Элемент. (этот способ отобразит Вам код того элемента, на котором вы щёлкнули правой кнопкой)

Вкладка Elements, Inspector

По умолчанию, в панели открывается вкладка Elements для Chrome и Inspector Firefox.


Этот инструмент позволяет Вам видеть, как HTML-код выглядит на странице в настоящем времени, а также как CSS стили, которые применены к каждому элементу на странице. Это также позволяет Вам в реальном времени редактировать как HTML, так и CSS. Внесённые изменения можно увидеть непосредственно в окне браузера.

С помощью контекстного меню вызванного на элементе можно выполнить следующие основные операции:

  • • Добавить/Редактировать класс или любой другой атрибут элемента.
  • • Спрятать или показать элемент
  • • Редактировать как HTML
  • • Копировать как HTML
  • • :hover/:active/:focus/:visited. Заставляет элементы переключить своё состояние на то, к которому применён стиль.

В правой части расположен редактор CSS стилей. В этом редакторе кроме прочих есть две основные вкладки это Стили (Styles) и Вычислено (Computed).
CSS редактор во вкладке Стили отображает CSS правила применённые к текущему выбранному элементу.


Нужно отметить что CSS правила, примененные к текущему элементу, отображаются в порядке убывания приоритета.
Основные функции CSS редактора:

  • • Можно убирать галочки напротив свойств, для того чтобы видеть, что получится, если их удалить;
  • • Нажав на имя свойства или его значение, можно открыть текстовое окошко, в котором Вы можете задать новые значения и увидеть, как изменится Ваш элемент с новыми значениями.
  • • Осуществлять поиск необходимых свойств/классов
  • • Показывать свойства элементов при выполнении на них каких либо действий (:hover/:active/:focus/:visited). Для этого необходимо активировать вкладку :hov

Рядом с каждым свойством указаны имя файла и номер строки, где располагается это свойство. Щелчок по этому пути перенесёт Вас в окно, где можно редактировать этот CSS и сохранить.

Во вкладке Вычислено (Computed) указаны все вычисленные свойства выделенного элемента, в том числе размеры элемента с отступами и границами, которые также можно редактировать и наблюдать за поведением выбранного элемента.

Вкладка Console

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


При выводе сообщений в консоль в правой ее части можно увидеть ссылку, в которой указан файл и номер строки, инициировавшей вывод сообщения. При клике на эту ссылку Вы будете перенаправлены на вкладку Sources, где будет отображен файл и непосредственно строка, которая вызвала сообщение в консоль.

Функционал вкладки Console также позволяет выполнять поиск выведенных сообщений.

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

Вкладка Sources

Вкладка Sources имеет 3 основных области:

  • • Область исходных файлов. В ней находятся все подключённые к странице файлы, включая JS/CSS
  • • Область содержимого выбранного файла, она же является редактором.
  • • Область информации и контроля

С помощью редактора можно устанавливать точки останова (паузы/breakpoint-ы), в которых выполнение скрипта будет приостанавливаться. Для установки breakpoint-а необходимо левой кнопкой мышки щелкнуть на номер строки, затем перезагрузить страницу. После перезагрузки страницы будет выведено сообщение Paused in debugger и выполнение скрипта приостановлено.
В области информации и контроля, находящейся справа от редактора, во вкладке Breakpoints будут отображены действующие точки останова.


Вкладка Breakpoints позволяет:

  • • Быстро перейти на место кода, где стоит брейкпойнт кликом на текст;
  • • Временно выключить брейкпойнт кликом на чекбокс;
  • • Быстро удалить брейкпойнт правым кликом на текст и выбором Remove, и так далее.

В правой части, кроме Breakpoints присутствует вкладка Watch , которая отображает текущие значения любых выражений. Можно раскрыть эту вкладку, нажать мышью «+» на ней и ввести любое выражение. Отладчик будет отображать его значение на текущий момент, автоматически перевычисляя его при проходе по коду. С помощью контекстного меню в редакторе можно установить наблюдение за нужной переменной.
Во вкладке Call Stack расположен стек вызовов, все вложенные вызовы, которые привели к текущему месту кода.
Также в редакторе можно увидеть значение вычисленных переменных, результатов выполнения функций и т.д.
Во вкладке Scope отображены все переменные. В Local показываются переменные функции, объявленные через var/let, параметры и ключевое слово this.
В Global – глобальные переменные и функции.

Для дальнейшего управления работой скрипта есть небольшая панель.

Основной функционал:

  1. Продолжить выполнение скрипта, горячая клавиша F8 — продолжает выполнения скрипта с текущего момента в обычном режиме. Если скрипт не встретит новых точек останова, то в отладчик управление больше не вернётся.
  2. Сделать шаг, не заходя внутрь функции, горячая клавиша F10 — выполняет одну команду скрипта. Если в ней есть вызов функции – то отладчик обходит его стороной, т.е. не переходит на код внутри.
    Эта кнопка очень удобна, если в текущей строке вызывается функция которая не представляет интереса. Тогда выполнение продолжится дальше, без захода в эту функцию.
  3. Сделать шаг, горячая клавиша F11 — выполняет одну команду скрипта и переходит к следующей. Если есть вложенный вызов, то заходит внутрь функции.
  4. Выполнять до выхода из текущей функции, горячая клавиша Shift+F11 — выполняет команды до завершения текущей функции. Эта кнопка очень удобна в случае, если случайно выполнен вход во вложенный вызов, из которого необходимо выйти.
  5. Деактивировать точки останова (отключает, но не удаляет их)
  6. Включить/Отключить автоматическую остановку при ошибке — когда она включена, то при ошибке в коде он автоматически будет остановлен, после чего можно посмотреть в отладчике текущие значения переменных, стек вызовов и т.д.

Вкладка Network

Во вкладке Network отображены сетевые взаимодействия. В табличном виде выведены загружаемые данные и файлы.


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

В следующей панели можно управлять видом отображаемых данных:

  1. Включение фильтра отображаемых данных
  2. Изменение вида отображения списка
  3. Время загрузки данных
  4. Позволяет заблокировать кеширование данных (все данные будут загружаться заново)
  5. Включает/отключает сеть. Позволяет имитировать пропадание связи во время загрузки
  6. Установка типа и скорости сети. Позволяет имитировать медленную скорость сети.

При активном блоке Filter можно выполнять поиск файла. Также можно выполнить сортировку по типу загружаемых данных JS/CSS/Img или различного рода Media файлы.

Вкладка XHR (XmlHttpRequest) — фильтрует данные полученные с помощью ajax-запросов.
Выделив один из запросов в представленной таблице можно получить более расширенную информацию о полученных данных. Такую как url, заголовок, метод, статус запроса и т.д.
Во вкладке Preview отображена информация в формате JSON.

Настройка инструментов разработчика


Нажав на значок вертикального троеточия находящийся справа вверху панели разработчика можно активировать меню отображения и дополнительных вкладок.
С помощью этого меню можно менять расположения панели «Инструменты разработчика», а также перейти в настройки.

Отображение на мобильных устройствах


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

DOM

Window

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


Как видно из схемы, объект window служит связующим звеном для взаимодействия JavaScript с браузером. Объектная модель документа (Document Object Model – DOM) служит для работы со структурой документа, объектная модель браузера (Browser Object Model – BOM) – это сборное понятие, которое объединяет множество объектов, содержащих в себе свойства и методы для работы с браузером. В JavaScript не существует встроенных свойств и методов для работы DOM и BOM. Другими словами, сам по себе JavaScript не умеет работать ни с HTML страницей, ни с браузером, это возможно лишь благодаря глобальному объекту window, который предоставляет набор необходимых свойств и методов, благодаря которым JavaScript может всесторонне взаимодействовать с браузером.
Рассмотрим несколько свойств и методов для работы с браузером, которые вызываются на самом объекте window.
window.innerWidth – ширина области содержимого окна браузера.
window.innerHeight – высота области содержимого окна браузера.
window.outerWidth – внешняя ширина браузера, включая все элементы интерфейса.
window.outerHeight – внешняя высота браузера, включая все элементы интерфейса.
window.pageXOffset – число пикселей, на которое документ был прокручен по горизонтали.
window.pageYOffset – число пикселей, на которое документ был прокручен по вертикали.
Все перечисленные выше свойства доступны только для чтения.
Чтобы прокрутить страницу к необходимым координатам, существуют два метода:

  • • window.scrollTo() – прокручивает страницу к заданным координатам;
  • • window.scrollBy() – прокручивает страницу на заданное количество координат.
window.scrollTo(0, 500);
// прокрутить страницу к позиции 0 пикселей по горизонтали и 500 по вертикали

window.scrollBy(0, 100); 
// прокрутить страницу на 0 пикселей по горизонтали и на 100 по вертикали

Screen

Screen – объект который содержит информацию о экране посетителя.

Объект window.screen , как и все остальные объекты в window, может вызываться и без приставки window.
Наиболее полезные свойства данного объекта предназначены для получения информации о размерах экрана посетителя:

  • • screen.width – возвращает полную ширину экрана;
  • • screen.height – возвращает полную высоту экрана;
  • • screen.availWidth – возвращает ширину экрана доступную для веб контента, исключая таскбары;
  • • screen.availHeight – возвращает высоту экрана доступную для веб контента, исключая таскбары.

Location

Location – объект, который содержит информацию про текущий URL.
Наиболее часто используемые свойства для работы с URL:

  • • location.href – полный адрес (URL) текущей страницы;
  • • location.origin – содержит протокол, хост и порт текущего URL;
  • • location.hostname – доменное имя (хост);
  • • location.pathname – строка пути после имени хоста;
  • • location.protocol – текущий веб протокол.
// http://www.jsexpert.net/javascript-course/

location.href; // 'http://www.jsexpert.net/javascript-course/'
location.origin; // 'http://www.jsexpert.net'
location.hostname; // 'www.jsexpert.net'
location.pathname; // /javascript-course/
location.protocol; // 'http'

Все свойства доступны как для чтения, так и для записи. Таким образом, можно программно менять URL текущей страницы.
Методы объекта location:

  • • location.assign() – загружает новую страницу, URL которой передано в метод;
  • • location.replace() – заменит текущую страницу на страницу по указанному URL;
  • • location.reload() – перезагружает страницу по текущему URL. При передаче в метод, как параметр, true, страница будет заново загружена с сервера, при передаче false – страница загружается из кэша (если ничего не передать в метод, то по умолчанию метод сработает как false).
// http://www.jsexpert.net/javascript-course/

location.assign('http://www.jsexpert.net');
location.replace('http://www.jsexpert.net/courses');
location.reload(true);

Разница между методами assign и replace в том, что при использовании replace, предыдущий URL удаляется из истории, пользователь не может вернутся на предыдущую страницу с помощью кнопки «Назад» в браузере. При использовании assign, предыдущий URL остается в истории.
При присвоении свойствам или при передаче URL как параметров в методы, значения указываются в виде строк.

History

History – объект содержит историю URL посещенных пользователем.
history.length – количество URL сохраненных в истории браузера в текущем окне браузера. Максимальное значение данного свойства равно 50.
Методы:

  • • history.forward() – перейти на следующий URL из списка истории посещения;
  • • history.back() – перейти на предыдущий URL из списка истории посещения;
  • • history.go() – перейти на конкретный URL из списка истории посещения, относительно текущей страницы.
history.forward(); // перейти на страницу вперед
history.back(); // перейти на страницу назад

history.go(3); // перейти на 3 страницы вперед
history.go(-2); // перейти на 2 страницы назад

Cookie и Web Storage

Cookie – данные о пользователе которые хранятся в браузере и передаются на сервер.
Когда веб сервер, в ответ на запрос браузера, присылает страницу, соединение закрывается и сервер «забывает» о текущем посещении. Бывают случаи, когда при повторном обращении к серверу, необходимо знать некую информацию о пользователе. Например, при первом посещении сайта сервер ничего не знает о том, кто именно его посетил. После первого посещения пользователь может предоставить некую информацию о себе, и эта информация записывается в куки. При следующих посещениях сайта, вместе с запросом будут отправлены сохраненные куки и сервер уже будет знать, кто именно посещает сайт.
Это может быть использовано, к примеру, для регистрации на сайте. Данные о том, что пользователь зарегистрировался и вошел на сайт, записываются в куки и теперь сервер знает, что при запросе страницы, например, личного кабинета, необходимо вернуть информацию именно для этого пользователя.
Свойство cookie вызывается на объекте document , а не window.
Чтобы установить куки необходимо передать в свойство cookie строку в которой будут указаны свойства со значениями перечисленные через точку с запятой.

document.cookie = 'username=John; expires=Thu, 20 Dec 2018 12:00:00 UTC; path=/; domain=""; secure';

Первое свойство устанавливает имя куки и его значение (username=John). Имя куки, как и его значения, могут быть произвольны.
Следующее свойство expires устанавливает дату истечения срока хранения куки с текущим именем. Если это свойство не указать, то куки будет удалено после закрытия страницы браузера. Такая дата может быть создана с помощью метода toUTCString(), который преобразует дату в строку, используя часовой пояс UTC, используя объект Date. Например:

let currentDate = new Date();
currentDate.setDate(currentDate.getDate() + 1);

let expireDate = currentDate.toUTCString();
console.log(expireDate);

Сформирует дату для следующего дня от текущего.
Если будет указана дата, что уже прошла, то куки будет удалено.
Свойство path указывает путь которому принадлежит куки. Если оставить пустым, то имеется ввиду что куки доступно всем путям от текущего. Но если указать значение, то куки будет доступно только при посещении указанного пути. Например, указав значение /courses/, куки будет доступно только при посещении страницы с адресом jsexpert.net/courses/. Как правило значение этого свойства оставляют пустым или со значением слеш (/).
Свойство domain устанавливает домен на котором доступно куки. Если не указать, то куки будет доступно с домена, с которого было установлено соединение. Например, если явно указать значение jsexpert.net, куки будет доступно только на этом домене, а если указать .jsexpert.net, то будет доступно как на домене так и всех поддоменах.
Если же установить свойство secure , то куки будет передано на сервер только при соединении по HTTPS.
Для получения всех куки на текущей странице необходимо просто обратиться к свойству cookie – document.cookie .
Максимальный размер куки – 4 Кб .
Для удаления и получения куки по значению, стандартных функций не предусмотрено, все они реализуются вручную.
Web Storage – место для локального хранения данных в браузере.
Другими словами, технология Web Storage позволяет хранить данные на стороне клиента, которые никогда не передаются на сервер.
Существуют два объекта для хранения данных на стороне клиента, которые принадлежат объекту window:

  • • localStorage – позволяет хранить данные локально без ограничений по времени хранения и могут быть удалены только с помощью JavaScript;
  • • sessionStorage – позволяет хранить данные локально на протяжении одной сессии (до тех пор, пока не будет закрыта вкладка браузера).

Методы для работы с localStorage и sessionStorage одинаковы. Рассмотрим все методы на примере localStorage:

  • • localStorage.setItem() – добавление значения в хранилище;
  • • localStorage.getItem() – получение значения из хранилища по ключу;
  • • localStorage.removeItem() – удалить значение по ключу;
  • • localStorage.clear() – очистить хранилище.
localStorage.setItem('site', 'jsexpert.net');
// добавили данные, которые можно получить по ключу site

let site = localStorage.getItem('site');
console.log(site); // 'jsexpert.net'

localStorage.removeItem('site');

localStorage.clear();

Данные устанавливаются в формате ключ значение.
Если при добавлении данных в хранилище значение с таким ключом уже будет существовать, то значение будет перезаписано. Хранение одинаковых ключей недопустимо.
Так как localStorage и sessionStorage являются обычными объектами, то для добавления, получения или удаления значений, можно использовать синтаксис привычный для работы с объектами.

localStorage['site'] = 'jsexpert.net' // установка значения
localStorage['site']; // получение значения
delete localStorage['site'] // удаление значения

Максимальный размер для хранения данных в Web Storage составляет 5 Мб .
На Cookie и Web Storage можно посмотреть и в браузере.


Как видно, переданные в куки значения парсятся и подставляются в соответствующие колонки.

Web Storage хранит данные в формате ключ (key) значение (value).
Для Local Storage и Session Storage созданы отдельные вкладки.

Popup (alert, confirm и prompt)

Alert Box – выводит на экран модальное окно с сообщением, которое блокирует дальнейшее выполнение JavaScript, пока пользователь не закроет его.

alert('Hello');

Confirm Box – модальное окно с сообщением и кнопками для подтверждения или отклонения.
После того, как пользователь нажмет «Ok», то в JavaScript вернется true, если же нажать «Cancel», то вернется false.

const confirmAge = confirm('Are you 18 years old?');

if (confirmAge === true) {
    alert('Allowed');
} else {
    alert('Denied');
}

Prompt Box – модальное окно, которое позволяет получить от пользователя данные.
После введения информации и нажатия «Ok», JavaScript получит введенную пользователем информацию в виде строки. При нажатии «Cancel» возвращается null. Если нажать «Ok» без введенной информации, вернется пустая строка.

const name = prompt('Please enter your name');

if (name === null || name === '') {
    alert('Hello user!');
} else {
    alert(`Hello ${name}!`);
}

Метод может принять и второй параметр, который означает значение по умолчанию.

const name = prompt('Please enter your name', 'someone');

alert(`Hello ${name}!`);

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

Timing (setTimeout и setInterval)

setTimeout() – позволяет вызвать переданную в метод функцию через определенный отрезок времени.
setInterval() – позволяет вызывать функцию повторно с указанным временным интервалом.

setTimeout(function() {
    console.log('Time is out!');
}, 3000);

setInterval(function() {
    console.log('Interval');
}, 500);

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

const setFunction = function() {
    console.log('Time is out!');
};

setTimeout(setFunction, 3000);

setInterval(() => console.log('Interval'), 500);

Каждый метод возвращает уникальный идентификатор, который позволяет отменить вызов функции с помощью setTimeout или остановить интервальный запуск функции с помощью setInterval. Для этого существуют соответствующие методы clearTimeout и clearInterval .

const stopTimeout = setTimeout(() => console.log('Time is out!'), 3000);

const stopInterval = setInterval(() => console.log('Interval'), 500);

clearTimeout(stopTimeout);
clearInterval(stopInterval);

Результаты вызовов методов сохраняются в переменные. При необходимости отменить или остановить выполнение, эти переменные передаются в соответствующие методы для остановки.

ДОМАШНЕЕ ЗАДАНИЕ

Архив с заготовкой можно скачать здесь.

Задача: Вам необходимо построить галерею изображений одним из 3-х способов.

  1. методом replace
  2. методом шаблонных строк
  3. методом createElement

Общее описание: необходимо взять данные, которые находятся в файле data.js преобразовать эти данные и отобразить их на экране. Для отображения на экране необходимо реализовать цикл который создаст нужный html код для последующей вставки в страницу.
Создавать html код можно одним из трех возможных способов. Два этих способа показаны в заготовке. Третий освещался в лекции на тему DOM.

Рекомендация: задание объемное, постарайтесь реализовать столько сколько сможете. Если что-то не поняли, пропускайте или делайте как поняли.

Пошаговый алгоритм:

  • • Взять данные из data.js.
    Из исходных данных отобразить только саму картинку, название, описание, и дату. Соответственно для этих данных использовать преобразования, которые были в предыдущем домашнем задании. Остальные данные отбросить.
  • • Получить результирующий массив объектов.
    Определить тип галереи, которую будете строить. Для этого снять значение из первого селектбокса, пример как это сделать указан в коде. Вы получите значения 0, 1, 2 и тд. Содержимое атрибута value.
  • • Определить количество элементов, которые будете показывать в галерее. Для этого снять значение со второго селектбокса.
  • • После нажатия на кнопку «построить галерею» запустить функцию run или init которая построит галерею.
  • • Проитерироваться по массиву объектов с помощью цикла. Количество итераций зависит от выбранного значения во втором селектбоксе.
  • • На каждой итерации цикла HTML код создавать с помощью метода который был выбран в первом селектбоксе. Примеры создания HTML кода есть в заготовке.
  • • При построении галереи должны быть скрыты все блоки, которые не относятся к данному типу галереи. Соответственно будет отображен только один заголовок и сама галерея. Каждый такой блок находится внутри контейнера с классом (first-group, second-group, third-group). Таким образом, например, если показан блок second-group то все остальные блоки скрыты.
  • • Если пользователь меняет параметры изменив значения в селектбоксе, после нажатия на кнопку «построить галерею» она должна быть пересоздана согласно новым параметрам.
    Например изменился способ построения, тогда прячем блок second-group и показываем тот который необходимо и перестраиваем галерею новым способом. При этом заново считываем значения параметра из второго селектбокса и строим то количество элементов, которе необходимо.

Задача «Часы»
Вам необходимо реализовать электронные часы с календарем, которые будут показывать текущее время и дату.
1. Воспользуйтесь шаблоном по ссылке для визуального оформления часов. Заготовку сможете найти здесь.
2. Создайте самостоятельно скриптовый файл и подключаете его к HTML странице. Вся логика по работе приложения должна располагаться в этом файле.
3. Для реализации часов воспользуйтесь функционалом встроенного объекта Date и функциями setInterval.
4. При загрузке приложения, с помощью new Date() получите значение текущего времени и даты и отобразите его на экране.
5. С помощью функции setInterval каждую секунду (или чаще), запускать специальную функцию, которая будет перерисовывать значение текущего времени если это необходимо.
6. Таким образом у вас на экране должна отображаться такая информация: Сегодня среда, 10 января. Текущее время: 14:25:45
7. Так же необходимо отобразить информацию сколько дней осталось до следующего года. Формат: До 2019 года осталось 327 дней.

ОСНОВЫ РАБОТЫ С СОБЫТИЯМИ

События и их виды

Когда пользователь совершает действия на сайте (клик мышкой, нажатие на кнопку клавиатуры) или сам браузер производит какое-то действие (загрузка страницы, изменение размеров окна), происходят так называемые события .
Событие – это внутренний механизм JavaScript, который генерирует сигнал о том, что совершено какое-то действие. В прикладном смысле, это позволяет вам запускать определенные функции, когда происходят те или иные действия, инициированные браузером или пользователем.
DOM дает возможность JavaScript реагировать на эти события, поскольку стандартные события происходят непосредственно в браузере.
Примером таких событий может быть окончание загрузки страницы, изменение содержимого input поля, клик по элементу на странице и многое другое.
Существует множество событий и их видов: события мыши, клавиатуры, документа и т.п. Вот несколько из них:

  • • click – событие происходит при клике мышью;
  • • mouseover – событие происходит при наведении мыши на элемент;
  • • mousemove – событие происходит при перемещении курсора мыши;
  • • keypress – событие происходит при нажатии пользователем на клавишу на клавиатуре;
  • • scroll – событие происходит при прокручивании страницы;
  • • submit – событие происходит при отправке формы;
  • • focus – событие происходит при фокусировке на элементе input;
  • • blur – событие происходит при потере элементом фокуса;
  • • resize – событие происходит при изменении размера окна;
  • • DOMContentLoaded – событие происходит, когда HTML загружен и обработан.

Назначение обработчика событий

Обработчиком событий является функция, которая выполняется после того, как событие произошло.
Способов назначить обработчик событий есть несколько.

Использование HTML атрибута события

Данный способ заключается в том, что обработчик событий указывается непосредственно в HTML разметке, на необходимом HTML элементе, с помощью специальных атрибутов событий.
Все такие атрибуты легко запомнить, так как они совпадают с названиями самих событий, только с приставкой «on». Например, onclick, onchange и т.п.

<input type="text" onfocus="console.log('It works')">

<button onclick="clickHandler()">Button</button>

<script>
    function clickHandler() {
        console.log('Pressed');
    }
</script>

В примере с input полем используется атрибут onfocus, значением которого является обработчик события в виде стандартного console.log(). Данное событие происходит, когда текстовое поле получает фокус.
Пример с кнопкой демонстрирует возможность создания функции отдельно и лишь потом передать ее как обработчик событий в атрибут. В данном случае отдельно создается функция, в которой описываются необходимые манипуляции. Далее эту функцию просто передают как обработчик события в атрибут onclick, обратите внимание, функцию указывают вместе с круглыми скобками. Это означает, что при клике на кнопку, данная функция будет вызвана.

Использование DOM-свойства

В DOM есть свойства, которые отвечают за события. Называются они так же, как и HTML атрибуты.

<button>Button</button>

<script>
    const button = document.querySelector('button');
    
    function clickHandler() {
        console.log('Pressed');
    }

    button.onclick = clickHandler;
</script>

На найденном в DOM элементе применяем свойство, отвечающее за событие, и назначаем ему обработчик события. Обратите внимание, в данном случае функция указывается без круглых скобок. Это связано с тем, что во время выполнения кода, интерпретатор дойдет до места где назначается обработчик события с круглыми скобками и поймет это как вызов функции и выполнит ее. Таким образом, функция автоматически выполнится один раз при запуске JavaScript кода. А вот при наступлении события функция-обработчик не будет реагировать на него.
По сути, рассмотренные два способа назначения обработчика событий идентичны. Используя способ назначения обработчика с помощью атрибута, на самом деле происходит инициализация соответствующего DOM-свойства.
Оба способа считаются устаревшими и на практике практически не встречаются.
Большим недостатком этих способов, является то, что невозможно назначить несколько обработчиков на одно событие.

<button>Button</button>

<script>
    const button = document.querySelector('button');
    
    function clickHandler() {
        console.log('Pressed');
    }

    function buttonHandler() {
        console.log('Clicked');
    }

    button.onclick = clickHandler;
    button.onclick = buttonHandler;
</script>

В данном случае второе назначение обработчика события перетрет первое. Выполнится только buttonHandler.

С помощью метода addEventListener

Использование метода addEventListener является современным способом назначения события.
Назначение обработчика события осуществляется путем вызова метода addEventListener на элементе на котором необходимо «ловить» событие.
Сам метод принимает три параметра, два обязательных, третий опциональный.
Первый параметр – это имя события (click, change, mouseover и т.д.).
Второй параметр – функция-обработчик события.
Третий параметр – стадия обработки события (всплытие или захват).

<button>Button</button>

<script>
    const button = document.querySelector('button');
    
    button.addEventListener('click', function() {
        console.log('Clicked');
    });
</script>

С помощью этого способа можно добавлять несколько обработчиков событий на один элемент.

<button>Button</button>

<script>
    const button = document.querySelector('button');
    
    function clickHandler() {
        console.log('Pressed');
    }

    function buttonHandler() {
        console.log('Clicked');
    }

    button.addEventListener('click', clickHandler);
    button.addEventListener('click', buttonHandler);
</script>

Функции-обработчики можно непосредственно прописывать в методе addEventListener или же предварительно сохранять их в переменные и передавать как параметр в метод, без круглых скобок.
События можно устанавливать на любые DOM элементы.

window.addEventListener('resize', function() {
    console.log(window.innerWidth);
});

Третий параметр, будет рассмотрен при разборе вопроса о порядке работы событий.

Удаление обработчика события

Так как выполнение функций-обработчиков, как и любого другого кода, забирает ресурсы, ненужные обработчики необходимо удалять, для оптимизации работы кода. Особенно это актуально при выполнении обработчиков для таких событий как scroll и resize, которые срабатывают каждый раз, когда пользователь прокручивает или меняет размер страницы. Например, при прокрутке до определенного места на странице, отпадает необходимость в выполнении функции-обработчика, которая назначена на событие scroll. В таком случае, лучше удалить этот обработчик, что бы он не выполнялся зря и не тратил ресурсы.
Удалить обработчик события, установленного с помощью метода addEventListener, можно, использовав метод removeEventListener .
Следует учитывать то, что для удаления обработчика, необходимо указывать именно установленную ранее функцию-обработчик, а не просто продублировать функцию с одинаковым кодом. Для этого необходимо сохранить функцию-обработчик в переменную и передать ее как параметр в метод addEventListener и, при необходимости удалить обработчик, в removeEventListener.

<button>Button</button>
    
<script>
    const button = document.querySelector('button');

    let handler = function(event) {
        console.log('Clicked');
    }

    button.addEventListener('click', handler);
    button.removeEventListener('click', handler);
</script>
 

Если же функцию handler прописать непосредственно в каждом методе (addEventListener и removeEventListener), то обработчик будет назначен, но не удалится.
Что бы удалить обработчик, установленный с помощью DOM-свойства, необходимо вместо функции-обработчика назначить null.

<button>Button</button>

<script>
    const button = document.querySelector('button');
    
    function clickHandler() {
        console.log('Pressed');
    }

    button.onclick = clickHandler;
    button.onclick = null;
</script>

### ПРИЕМЫ ДЛЯ РАБОТЫ С СОБЫТИЯМИ

Порядок работы событий

При разборе вопроса о назначении обработчика событий, упоминалось о том, что метод addEventListener принимает три параметра, два из которых были рассмотрены.
Вернемся к третьему параметру метода, который определяет стадию обработки события. В него передается булевое значение:
true – означает, что событие отработает на стадии захвата (capturing);
false – означает, что событие отработает на стадии всплытия (bubbling);
По умолчанию значение установлено false, поэтому, не передав значение третьего параметра, событие отрабатывает на стадии всплытия.
Всплытие – когда инициализируется событие, обработчик срабатывает на элементе, на котором оно было проинициализировано, после чего, событие, начинает подниматься вверх, к родительским элементам, и отрабатывать везде, где установлен обработчик события на это же событие.

<div id="first">First div
    <div id="second">Second div
        <div id="third">Third div</div>
    </div>
</div>
    
<script>
    const div1 = document.querySelector('#first');
    const div2 = document.querySelector('#second');
    const div3 = document.querySelector('#third');

    let handler = function(event) {
        alert(event.currentTarget.id);
    }

    div1.addEventListener('click', handler, false);
    div2.addEventListener('click', handler);
    div3.addEventListener('click', handler);
</script>

В примере выше, если кликнуть мышью на самом вложенном элементе, то выведется «third», но так как событие всплывает выше, к родительским элементам, оно отработает и на остальных двух тегах div. Таким образом вы так же увидите «second» и «first». Если же кликнуть на div с id second, то выведется только «second» и «first».
События всплывают вплоть до document.
Так же, в примере специально только для div1 указан третий параметр false, который показывает, что по умолчанию в двух других случаях, параметр все равно установлен в false. Следовательно даже не указав его, все равно обработчики событий сработают на стадии всплытия.
Пока что упустим объяснения, откуда в функции handler появился аргумент event, продолжим рассматривать обработку событий.
Захват – события «погружаются» сверху-вниз.

<div id="first">First div
    <div id="second">Second div
        <div id="third">Third div</div>
    </div>
</div>
    
<script>
    const div1 = document.querySelector('#first');
    const div2 = document.querySelector('#second');
    const div3 = document.querySelector('#third');

    let handler = function(event) {
        alert(event.currentTarget.id);
    }

    div1.addEventListener('click', handler, true);
    div2.addEventListener('click', handler, true);
    div3.addEventListener('click', handler, true);
</script>

При такой отработке событий, все происходит с точностью до наоборот, событие погружается от родительского элемента вниз, до целевого, на котором событие было проинициализировано. Кликнув по div с id «third» выведется сначала «first», далее «second» и в конце «third».
Полный цикл отработки событий состоит из трех фаз: захват или погружение (capturing) , достижение цели (target) и всплытие (bubbling) .
Достижение цели – это сам факт того, что событие дошло до элемента, на котором оно было проинициализировано.
На практике, чаще всего, используется всплытие. События, которое обрабатываются с помощью HTML атрибута или DOM-свойства, так же используют всплытие.
Стоит также отметить, что JavaScript всегда выполняется в один поток. В один момент времени может выполняться только одно действие. В этот поток попадает вся работа JavaScript кода, будь то работа с DOM или выполнение функции. В эту же очередь попадает и выполнение событий.
В один момент времени может выполняться только одно действие, соответственно только одно событие. Когда возникают несколько событий то все они попадают в очередь выполнения и исполняются одно за другим. Как только одно событие отработает, в очередь попадает следующее и так далее, пока очередь не очиститься.

Объект event

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

<button id="main" class="main-button">Button</button>
    
<script>
    const button = document.querySelector('button');

    let handler = function(event) {
        console.log(event);
    }

    button.addEventListener('click', handler);
</script>

Этот аргумент можно назвать как угодно, но чаще всего, для лучшей читаемости кода, его именуют «event» или просто «е».
Если посмотреть на этот объект в консоли, то можно увидеть, что это объект с весьма обширными данными о событии.


Пройдемся по некоторым из них.
Свойства:

  • • event.target – целевой элемент на котором событие было проинициализировано;
  • • event.currentTarget – элемент на котором в данный момент отработало событие и вызвался обработчик;
  • • event.type – тип события;
  • • event.clientX и event.clientY – координаты курсора, относительно окна, в момент срабатывания события.

Наиболее часто используемые свойства это target и currentTarget. Разница между этими свойствами в том, что в свойство target – попадает элемент на котором непосредственно произошло событие, например, кликнул пользователь.
А currentTarget – это элемент, где обработчик выполнился в данный момент. Это можно увидеть и использовать, например, во время всплытия события.

<div id="first">First div
    <div id="second">Second div
        <div id="third">Third div</div>
    </div>
</div>
    
<script>
    const div1 = document.querySelector('#first');
    const div2 = document.querySelector('#second');
    const div3 = document.querySelector('#third');

    let handler = function(event) {
        alert(event.target);
        alert(event.currentTarget);
    }

    div1.addEventListener('click', handler);
    div2.addEventListener('click', handler);
    div3.addEventListener('click', handler);
</script>

В данном примере, если кликнуть по div с id third, то target, при каждой отработке события, будет <div id=»third»>Third div</div>, а вот currentTarget будет меняться по мере всплытия события, сначала это будет <div id=»third»>Third div</div>, далее <div id=»second»>…</div> и на последок <div id=»first»>…</div>.
С полученными элементами можно работать, как и с теми, что получаем во время поиска элементов в DOM с помощью уже известных методов, так как это те же DOM элементы.
Кроме свойств, объект события содержит в себе и методы, рассмотрим некоторые из них:

  • • event.stopPropagation() – прекращение всплытия или захвата события;
  • • event.stopImmediatePropagation() – кроме прекращения всплытия или захвата события, препятствует выполнению всех событий на текущем элементе.
  • • event.preventDefault() – предотвращает стандартное поведение браузера.

Разница в методах stopPropagation и stopImmediatePropagation, опять же, видна при всплытии или захвате. Метод stopPropagation прекращает всплытие или захват, событие выполнится единожды, на элементе где оно было проинициализировано. Но в случае если на элементе несколько событий, то они дальше выполнятся на нем. В случае необходимости предотвратить выполнение не только текущего события при всплытии или захвате, но и всех остальных событий на исполняемом элементе, используется метод stopImmediatePropagation.

<div id="first">First div
    <div id="second">Second div
        <div id="third">Third div</div>
    </div>
</div>
    
<script>
    const div1 = document.querySelector('#first');
    const div2 = document.querySelector('#second');
    const div3 = document.querySelector('#third');

    let handler = function(event) {
        console.log('Click on ' + event.currentTarget.id);
        event.stopPropagation();
    }

    let buttonHandler = function(event) {
        console.log('Second click');
    }

    div1.addEventListener('click', handler);
    div2.addEventListener('click', handler);
    div3.addEventListener('click', handler);
    div3.addEventListener('click', buttonHandler);
</script>

В примере выше, с помощью метода stopPropagation будет остановлено всплытие, но вторая функция-обработчик buttonHandler все же выполнится. При клике на div с id third выведется ‘Click on third’ и ‘Second click’.

<input type="text">

<script>
    function down(event) {
        console.log('Down');
        event.stopImmediatePropagation();
    }

    function up() {
        console.log('Up');
    }

    const input = document.querySelector('input');
    input.addEventListener('keydown', down);
    input.addEventListener('keydown', up);
</script>

Это пример показывает, как работает метод stopImmediatePropagation. Без его использования, при нажатии на любую клавишу в пределах input выводится ‘Down’, а отпустив клавишу, выводится ‘Up’. Но использовав метод stopImmediatePropagation, выполняется только функция-обработчик назначенная на событие keydown.
Метод preventDefault останавливает стандартное поведение браузера и дает возможность вместо него, выполнить какое-то свое действие.

<a href="https://www.google.com.ua/">Link</a>

<script>
    function visitSite(event) {
        event.preventDefault();
        console.log('Prevented!');
    }

    const link = document.querySelector('a');
    link.addEventListener('click', visitSite);
</script>

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

Делегирование события

Благодаря всплытию возможно реализовать прием разработки, который называется делегирование (Event Delegation).
Суть подхода заключается в том, что если есть необходимость на нескольких элементах установить одинаковые обработчики события, то вместо того, чтобы назначать функцию-обработчик отдельно на каждом элементе, ставится один обработчик на их общего родителя.
С помощью event.target определяется на каком элементе отработало событие.

<div id="wrapper">    
    <div id="first">First div
        <div id="second">Second div
            <div id="third">Third div</div>
        </div>
    </div>
</div>
    
<script>
    const wrapper = document.querySelector('#wrapper');
    
    function handler(e) {
        console.log(e.target);
    }
  
    wrapper.addEventListener('click', handler);
</script>

Установив обработчик события на родительский элемент с id wrapper, можно отслеживать события на каждом из вложенных элементов. С помощью свойства event.target можно отследить на каком именно элементе событие было проинициализировано.

Собственные события

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

* • bubbles – возможные значения true или false, что указывает, может ли событие всплывать;

  • • cancelable – возможные значения true или false, что указывает, можно ли событие отменить с помощью метода stopPropagation;
  • • detail – значением является вложенный объект, с помощью которого можно предоставить информацию, которая будет передана вместе с событием.

Чтобы проинициализировать созданное событие, существует специальный метод dispatchEvent .

<input type="text" id="message">
<br>
<br>
<textarea id="output"></textarea>

<script>
    const input = document.getElementById('message');
    const output = document.getElementById('output');

    function keyPressHandler(event) {
        let value = event.currentTarget.value;

        const customEvent = new CustomEvent('listener', {
            detail: {
                message: value
            },
            bubbles: true,
            cancelable: true
        });

        event.currentTarget.dispatchEvent(customEvent);
    }

    function messageHandler(event) {
        output.textContent = event.detail.message;
    }

    input.addEventListener('keyup', keyPressHandler);
    document.addEventListener('listener', messageHandler);
</script>

В примере выше на поле input, на событие keyup, определяем функцию-обработчик в которой получаем значение, введенное в поле. После этого создаем свое событие «listener» и передаем в него полученное значение с поля input и затем с помощью dispatchEvent инициализируем событие.
На document, с помощью addEventListener, слушаем наступление события listener. При нажатии клавиши в текстовом поле, срабатывает обработчик keyPressHandler в котором инициализируется событие listener. Среагировав на это событие, запускается функция-обработчик messageHandler, в которой с помощью event.detail.message получаем переданное в событие listener параметр с введенным текстом в текстовое поле и передаем его в textarea.

ДОМАШНЕЕ ЗАДАНИЕ

Задача: Расширить функциональность галереи. Добавить интерактивности, используя возможности работы с событиями.
То есть вам необходимо взять результат того, что вы получили в прошлом домашнем задании и расширить функциональность вашего приложения новыми пунктами.
Полное разъяснение ДЗ в видео. Здесь даны пункты в короткой форме для справки.

Пример приложения: Галерея

1. По ссылке выше доступен рабочий пример того, что у вас должно получиться.
2. Для построения галереи использовать подход с шаблонными строками.
3. Selectbox с выбором количества элементов для отображения и выбором типа галереи убрать. То есть всегда строить все изображения с помощю шаблонной строки.
4. Добавлять новый элемент в галерею после нажатия на соответствующую кнопку.
5. Отображать количество доступных элементов, которые еще можно добавить.
6. При добавлении полностью всех элементов, сделать кнопку «Добавить» серого цвета.
7. Если пользователь все таки нажал на кнопку, выводить модальное окно с сообщением что дальнейшее добавление невозможно.
8.* Сделать модальное окно не с помощью Аlert, а с помощью специального компонента Modal из фреймворка Bootstrap 3 или Bootstrap 4.
9.* Реализовать механизм удаления картинки. Для этого использовать патерн «делегация». То есть обработчик события должен быть не на каждой кнопке «удалить», а на элементе верхнего уровня.
10. Расширить галерею сортировкой (selectbox). Содержит 4 пункта: «По имени А- Я», «По имени Я — А», «Сначала новые», «Сначала старые».
11. При изменении выбора в selectbox галерея перестраивается автоматически. Нажатие на кнопку не требуется. Строится то количество изображений, которое было на экране. Если вы добавили 6 картинок, то и после изменения сортировки их должно быть так же 6. При первой загрузке, по умолчанию применяется фильтр «По имени А- Я».
12. Значение фильтра сохраняется в localStorage. Таким образом, после перезагрузки страницы автоматически должен применяться последний выбранный фильтр.

РАБОТА С THIS

this в функциях

Когда вы вызываете функцию и внутри нее используете this – это глобальный объект window .

function checkThis() {
    console.log(this === window);
}

checkThis(); // true

Если же функция вызывается с использованием директивы use strict (вызывается в строгом режиме), то this буде равен undefined .

function checkThis() {
    'use strict';
    console.log(this === undefined);
}

checkThis(); // true

Строгий режим также активен и во всех вложенных функциях, по этой причине this, во вложенных функциях, так же будет равен undefined.

'use strict';
function checkThis() {
    function innerFunction() {
        console.log(this === undefined);
    }

    innerFunction();
}

checkThis(); // true

В функции, this всегда ссылается на объект, который вызывает эту функцию как метод. Именно поэтому если функция вызывается просто как функция, а не как метод какого-либо объекта – значение this ссылается на глобальный объект (window). В реальной работе такое поведение (использование window) часто приводит к ошибкам и практически никогда не используется, по этой причине, в строгом режиме, если функция вызывается сама по себе, значением this определенно, как undefined.

this в объектах

В методах, this ссылается на объект, в контексте которого вызван метод.
Метод – это функция, которая хранится в объекте.

let user = {
    checkThis: function() {
        console.log(this === user);
    }
}

user.checkThis(); // true

checkThis является методом объекта user. Все методы вызываются на объекте, в контексте которого необходимо выполнить метод. Другими словами, сначала указываем объект, метод которого необходимо выполнить, и далее через точку непосредственно указываем метод. Если очень упростить, то this равен объекту перед точкой.
При вызове метода, с использованием this внутри него, вместо самого this будет подставляться имя объекта в контексте которого вызывается метод.

let user = {
    firstName: 'Dave',
    lastName: 'Dowson',
    getFullName: function() {
        console.log(`${this.firstName} ${this.lastName}`);
        console.log(`${user.firstName} ${user.lastName}`);
    }
}

user.getFullName();
// Dave Dowson
// Dave Dowson

Пример выше явно иллюстрирует то, что вместо this, при вызове метода, подставляется объект, на котором этот метод был вызван.
Использование this чрезвычайно полезно, это позволяет разрабатывать очень гибкий код, который можно использовать многократно, не повторяя постоянно одинаковые части кода.
Полезность this, полностью раскрывается при разборе тем по ООП. Но рассмотрим небольшой пример сейчас.

let user1 = {
    firstName: 'Dave',
    lastName: 'Dowson',
}

let user2 = {
    firstName: 'Bob',
    lastName: 'Gordon',
}

function fullName() {
    console.log(`${this.firstName} ${this.lastName}`)
}

user1.getFullName = fullName;
user2.getFullName = fullName;

user1.getFullName(); // Dave Dowson
user2.getFullName(); // Bob Gordon

В глобальной функции fullName выводим в консоль полное имя пользователя, но при этом ко всем аргументам обращаемся через this. Далее, каждому из объектов, в качестве метода присваивается функция fullName, которая доступна по значению getFullName. Вызвав на каждом объекте метод getFullName, получили значение из соответствующего метода.
Как видите, использование this дало возможность единожды определить функцию, которую можно использовать как метод в любом объекте. Без использования this, пришлось бы создавать функцию для каждого объекта, в котором прямо бы указывалось user1.firstName и user2.firstName.

Потеря контекста

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

let user = {
    getFullName: function() {
        console.log(this === user);

        function innerFunction() {
            console.log(this === user);
        }

        innerFunction();
    }
}

user.getFullName();
// true
// false

При обращении к this во вложенной функции, часто ожидают, как результат, получить значение из текущего объекта. Но, как видно из примера выше, это не так. Давайте разберемся почему.
Замыкание не может получить доступ к значению this внешней функции (в отличии от других параметров), а все потому, что вложенная функция получает собственное значение this. Откуда берется это собственное значение? Ранее уже упоминалось, что, если функция вызывается просто как функция, а не как метод объекта – значение this ссылается на window или undefined в строгом режиме.
Как видно из примера, innerFunction вызывается как обычная функция. По этой причине, this, во вложенной функции, равен window. Контекст внутренней функции зависит только от того, как она вызвана, а не от контекста внешней функции.
Данная проблема актуальна как для методов, так и для обычных функций.
Есть несколько способов решения данной проблемы, которые будут рассмотрены в следующих темах.

this в стрелочных функциях

Проблема потери контекста не возникает при использовании стрелочной функции, как вложенной функции. Это связано с тем, что this стрелочной функции заимствует this из внешней функции, в которой она определена.
В данном случае this — это контекст , в котором определена стрелочная функция.
Если при работе с обычными функциями или методами для определения this имеет значение то, как вызвана функция или метод, а не контекст внешней функции, то для стрелочной функции ключевым моментом как раз является контекст внешней функции.

let user = {
    getFullName: function() {
        console.log(this === user);

        const innerFunction = () => {
            console.log(this === user);
        }

        innerFunction();
    }
}

user.getFullName();
// true
// true

Вложенная стрелочная функция всегда сохраняет контекст вызова внешней функции, она не создает свой собственный контекст.
При вызове стрелочной функции в глобальной области видимости, у нее, как у обычной функции, this равен либо window, либо undefined.

const globalFunction = () => {
    console.log(this === window);
}

globalFunction(); // true

this в событиях

В обработчиках событий this равен элементу на котором он сработал.

<div>Div</div>
<span>Span</span>

<script>
    const div = document.querySelector('div');
    const span = document.querySelector('span');

    function handler(event) {
        console.log(this);
    }

    div.addEventListener('click', handler); // <div>Div</div>
    span.addEventListener('click', handler); // <span>Span</span>
</script>

Через this мы получаем тот же элемент, что и с помощью event.currentTarget.

<ul id="list">
    <li>First</li>
    <li>Second</li>
    <li>Third</li>
    <li>Fourth</li>
</ul>

<script>
    const list = document.getElementById('list');

    function handler(event) {
        console.log(this);
        console.log(event.currentTarget);
        console.log(event.target);
        console.log(this === event.currentTarget);
    }

    list.addEventListener('click', handler);
</script>

На основе примера выше, следует обратить внимание, что если используется делегирование события, то this равен именно тому элементу, на который назначена функция-обработчик (event.currentTarget), то есть равен родительскому элементу, а не элементу на котором был произведен клик (event.target).
Следует помнить о том, что при использовании внутри функции-обработчика вложенной функции, то this уже не будет равен элементу, он будет равен глобальному объекту window или undefined (при выполнении в строгом режиме).

<div>Div</div>

<script>
    const div = document.querySelector('div');

    function handler(event) {
        console.log(this); // <div>Div</div>

        function innerFunction() {
            console.log(this); // Window
        }

        innerFunction();
    }

    div.addEventListener('click', handler);
</script>

Напомним, использовав стрелочную функцию, как вложенную, ее this будет равен this из внешней функции и как результат, получим тот же элемент.

<div>Div</div>

<script>
    const div = document.querySelector('div');

    function handler(event) {
        console.log(this); // <div>Div</div>

        const innerFunction = () => {
            console.log(this); // <div>Div</div>
        }

        innerFunction();
    }

    div.addEventListener('click', handler);
</script>
3 симпатии