#javascript #события #ecmascript_2015 #замыкания
Этот вопрос не дубликат, так как он затрагивает не только замыкания, но и иные распространённые ошибки в JS, а также их устранения используя новые возможности спецификации ES 2015. При переборе элементов не получается правильно зарегистрировать обработчики события click перебираемых элементов. События всех перебираемых элементов, регистрируются анонимной функцией, предназначенной для самого последнего элемента. По задумке. 1 При наведении на fieldset сообщения подсвечивается иконка удаления каждого сообщения, при отведении мышки иконка удаления, соответствующего сообщения, снова угасает. 2 При наведении на иконку удаления всплывает подсказка со временем соответствующего сообщения. 3 При клике на иконку соответствующего сообщения удаления всплывает алерт со временем соответствующего сообщения. Имею. 1 При наведении на любой fieldset сообщения подсвечивается иконка удаления последнего сообщения, при отведении мышки иконка удаления, последнего сообщения, снова угасает. 2 Это единственное, что работает правильно. 3 При клике на иконку удаления любого сообщения всплывает алерт со временем последнего сообщения. Что я делаю не так, что я тут не понимаю и как мне сделать правильно? window.addEventListener('load',function() { var imgs=document.getElementsByClassName('right')[0].getElementsByClassName('del'); for(var i=0,l=imgs.length;i0&&!isNaN(img.dataset.fid)&&img.dataset.fid>0&&img.dataset.title) alert('Are you sure you want to delete '+img.dataset.title+' message?'); }); } }); * { margin:0; padding:0; border:0; border-radius:5px; transition:all 0.2s linear; } html,body { height:100%; min-height:100%; } body { background-color:#fff; color:#000; font:12px/18px Arial,Helvetica,sans-serif; margin:0 auto; } fieldset { border:1px solid #ccc; } legend { font-weight:bold; } div.message { min-width:400px; width:400px; max-height:400px; margin:0 auto; padding:10px; text-align:center; } div.message div.left, div.message div.right { float:left; height:100%; overflow:auto; } div.message div.right { width:250px; text-align:center; } div.message div.right hr { border-top:1px solid #ccc; margin-top:10px; } div.message div.right hr+span { background-color:#fff; font-weight:bold; padding:0 10px; position:relative; bottom:10px; } div.message div.right fieldset { margin:0 0 10px; padding:10px; text-align:left; position:relative; } div.message div.right fieldset.from { margin-right:50px; border-color:#6f6; background-color:#dfd; color:#040; } div.message div.right fieldset.to { margin-left:50px; border-color:#66f; background-color:#ddf; color:#004; } div.message div.right fieldset img.del { width:16px; top:0; right:8px; } div.message img.del { position:absolute; opacity:0.3; } div.message img.del:hover { cursor:pointer; opacity:1; } Test Ответ: Самый простой и быстрый способ объявить все переменные через let вместо var.
Ответы
Ответ 1
На первый взгляд может показаться, что здесь классическая проблема с замыканием, и это она действительно и есть. Но! Решение здесь намного проще: так как в обработчике подразумевается работа с элементом по которому нажали, проблема замыкания решается использованием this, вместо переменной img. Так как, в данном случае this - будет, как раз тем элементом, по которому кликнули. window.addEventListener('load', function() { var imgs = document.getElementsByClassName('right')[0].getElementsByClassName('del'); for (var i = 0, l = imgs.length; i < l; i++) { var img = imgs[i]; var doc = document.getElementById('mes-' + img.dataset.date + '-' + img.dataset.fid); doc.addEventListener('mouseover', function() { // здесь проблема остается, так как обработчик вешается на fieldset img.style.opacity = 1; }); doc.addEventListener('mouseout', function() { // здесь проблема остается, так как обработчик вешается на fieldset img.removeAttribute('style'); }); img.setAttribute('title', 'Delete ' + img.dataset.title + ' message'); img.addEventListener('click', function() { if (!isNaN(this.dataset.date) && this.dataset.date > 0 && !isNaN(this.dataset.fid) && this.dataset.fid > 0 && this.dataset.title) alert('Are you sure you want to delete ' + this.dataset.title + ' message?'); }); } }); legend { font-weight: bold; } div.message { width: 400px; } div.message div.right fieldset { text-align: left; position: relative; border-color: #6f6; background-color: #dfd; color: #040; } div.message div.right fieldset.to { background-color: #ddf; } div.message div.right fieldset img.del { width: 16px; top: 0; right: 8px; position: absolute; opacity: 0.3; } Причины ошибки: Переменные объявленные с помощью ключевого слова var имеют функциональную область видимости. Это значит, что ни доступны внутри всей функции в которой объявлены и внутри объявленных вложенных функций. Несмотря на то, что объявление переменной находится внутри for будет объявлена всего одна переменная, значение которой будет меняться. Так как обработчики выполняются не сразу внутри цикла, а когда наступает определенное событие, они берут значение указанной переменной в момент выполнения, а это, в данном случае, значение присвоенное на последней итерации цикла. Возможные решения: Создание функции, создающей конкретный обработчик: в данном случае проблема решается за счет создание функции-фабрики, которая будет создавать функцию обработчика на основе параметра. Частным случаем этого подхода является использование IIFE, когда функция-фабрика вызывается сразу при объявлении Пример с именованной фабрикой: function createClickHandler(img) { return function() { if (!isNaN(img.dataset.date) && img.dataset.date > 0 && !isNaN(img.dataset.fid) && img.dataset.fid > 0 && img.dataset.title) alert('Are you sure you want to delete ' + img.dataset.title + ' message?'); }; } function createMouseoverHandler(img) { return function() { img.style.opacity = 1; }; } function createMouseoutHandler(img) { return function() { img.removeAttribute('style'); }; } window.addEventListener('load', function() { var imgs = document.getElementsByClassName('right')[0].getElementsByClassName('del'); for (var i = 0, l = imgs.length; i < l; i++) { var img = imgs[i]; var doc = document.getElementById('mes-' + img.dataset.date + '-' + img.dataset.fid); doc.addEventListener('mouseover', createMouseoverHandler(img)); doc.addEventListener('mouseout', createMouseoutHandler(img)); img.setAttribute('title', 'Delete ' + img.dataset.title + ' message'); img.addEventListener('click', createClickHandler(img)); } }); legend { font-weight: bold; } div.message { width: 400px; } div.message div.right fieldset { text-align: left; position: relative; border-color: #6f6; background-color: #dfd; color: #040; } div.message div.right fieldset.to { background-color: #ddf; } div.message div.right fieldset img.del { width: 16px; top: 0; right: 8px; position: absolute; opacity: 0.3; } Пример с IIFE: window.addEventListener('load', function() { var imgs = document.getElementsByClassName('right')[0].getElementsByClassName('del'); for (var i = 0, l = imgs.length; i < l; i++) { var img = imgs[i]; var doc = document.getElementById('mes-' + img.dataset.date + '-' + img.dataset.fid); doc.addEventListener('mouseover', function createMouseoverHandler(imgParam) { return function() { imgParam.style.opacity = 1; }; }(img)); doc.addEventListener('mouseout', function createMouseoutHandler(imgParam) { return function() { imgParam.removeAttribute('style'); }; }(img)); img.setAttribute('title', 'Delete ' + img.dataset.title + ' message'); img.addEventListener('click', function createClickHandler(imgParam) { return function() { if (!isNaN(imgParam.dataset.date) && imgParam.dataset.date > 0 && !isNaN(imgParam.dataset.fid) && imgParam.dataset.fid > 0 && imgParam.dataset.title) alert('Are you sure you want to delete ' + imgParam.dataset.title + ' message?'); }; }(img)); } }); legend { font-weight: bold; } div.message { width: 400px; } div.message div.right fieldset { text-align: left; position: relative; border-color: #6f6; background-color: #dfd; color: #040; } div.message div.right fieldset.to { background-color: #ddf; } div.message div.right fieldset img.del { width: 16px; top: 0; right: 8px; position: absolute; opacity: 0.3; } Использование let/const. Использование данных ключевых слов, которые появились в ES2015, позволяет объявлять переменные область видимости которых текущий блок. Это означает, что на каждой итерации цикла будет создаваться новая переменная, поэтому во время выполнения обработчика будет использоваться корректное значение window.addEventListener('load', function() { var imgs = document.getElementsByClassName('right')[0].getElementsByClassName('del'); for (var i = 0, l = imgs.length; i < l; i++) { let img = imgs[i]; var doc = document.getElementById('mes-' + img.dataset.date + '-' + img.dataset.fid); doc.addEventListener('mouseover', function() { img.style.opacity = 1; }); doc.addEventListener('mouseout', function() { img.removeAttribute('style'); }); img.setAttribute('title', 'Delete ' + img.dataset.title + ' message'); img.addEventListener('click', function() { if (!isNaN(img.dataset.date) && img.dataset.date > 0 && !isNaN(img.dataset.fid) && img.dataset.fid > 0 && img.dataset.title) alert('Are you sure you want to delete ' + img.dataset.title + ' message?'); }); } }); legend { font-weight: bold; } div.message { width: 400px; } div.message div.right fieldset { text-align: left; position: relative; border-color: #6f6; background-color: #dfd; color: #040; } div.message div.right fieldset.to { background-color: #ddf; } div.message div.right fieldset img.del { width: 16px; top: 0; right: 8px; position: absolute; opacity: 0.3; } Использование вместо for, функции forEach. Так как в данном случае в замыкании будет использован параметр функции, а не переменная, значение в момент выполнения обработчика события будет корректным. Из-за того, что функция getElementsByClassName возвращает не массив, а коллекцию, использовать у нее функцию forEach нельзя. Но можно преобразовать эту коллекцию в массив, либо использовать call. Пример: window.addEventListener('load', function() { var imgs = document.getElementsByClassName('right')[0].getElementsByClassName('del'); [...imgs].forEach(function(img) { var doc = document.getElementById('mes-' + img.dataset.date + '-' + img.dataset.fid); doc.addEventListener('mouseover', function() { img.style.opacity = 1; }); doc.addEventListener('mouseout', function() { img.removeAttribute('style'); }); img.setAttribute('title', 'Delete ' + img.dataset.title + ' message'); img.addEventListener('click', function() { if (!isNaN(img.dataset.date) && img.dataset.date > 0 && !isNaN(img.dataset.fid) && img.dataset.fid > 0 && img.dataset.title) alert('Are you sure you want to delete ' + img.dataset.title + ' message?'); }); }); }); legend { font-weight: bold; } div.message { width: 400px; } div.message div.right fieldset { text-align: left; position: relative; border-color: #6f6; background-color: #dfd; color: #040; } div.message div.right fieldset.to { background-color: #ddf; } div.message div.right fieldset img.del { width: 16px; top: 0; right: 8px; position: absolute; opacity: 0.3; } Использовать в обработчиках this, и искать элементы относительно элемента this. Так как, при добавлении обработчика с помощью addEventListener, внутри обработчика this будет указывать на элемент, к которому добавляли обработчик, то при клике на картинку, вместо замыкания можно использовать this. В случае, когда обработчик добавляется на fieldset можно найти нужную картинку внутри this, например с помощью querySelector Пример window.addEventListener('load', function() { var imgs = document.getElementsByClassName('right')[0].getElementsByClassName('del'); for (var i = 0; i < imgs.length; i++) { var img = imgs[i]; var doc = document.getElementById('mes-' + img.dataset.date + '-' + img.dataset.fid); doc.addEventListener('mouseover', function() { this.querySelector('img').style.opacity = 1; }); doc.addEventListener('mouseout', function() { this.querySelector('img').removeAttribute('style'); }); img.setAttribute('title', 'Delete ' + img.dataset.title + ' message'); img.addEventListener('click', function() { if (!isNaN(this.dataset.date) && this.dataset.date > 0 && !isNaN(this.dataset.fid) && this.dataset.fid > 0 && this.dataset.title) alert('Are you sure you want to delete ' + this.dataset.title + ' message?'); }); } }); legend { font-weight: bold; } div.message { width: 400px; } div.message div.right fieldset { text-align: left; position: relative; border-color: #6f6; background-color: #dfd; color: #040; } div.message div.right fieldset.to { background-color: #ddf; } div.message div.right fieldset img.del { width: 16px; top: 0; right: 8px; position: absolute; opacity: 0.3; } Делегация событий вместо цикла. В данном подходе обработчик вешается не на конкретную картинку, а на контейнер, где лежат все картинки. Внутри обработчика уже определяется по какому элементу щелкнули и выполняются нужные функции. Пример window.addEventListener('load', function() { var right = document.getElementsByClassName('right')[0]; right.addEventListener('click', function(e) { if (e.target.className === 'del') { if (!isNaN(e.target.dataset.date) && e.target.dataset.date > 0 && !isNaN(e.target.dataset.fid) && e.target.dataset.fid > 0 && e.target.dataset.title) alert('Are you sure you want to delete ' + e.target.dataset.title + ' message?'); } }); [...right.querySelectorAll('img')].forEach(img => img.setAttribute('title', 'Delete ' + img.dataset.title + ' message')); var fieldSets = right.querySelectorAll('fieldset'); for (var i = 0; i < fieldSets.length; i++) { fieldSets[i].addEventListener('mouseover', function() { this.querySelector('img').style.opacity = 1; }); fieldSets[i].addEventListener('mouseout', function() { this.querySelector('img').removeAttribute('style'); }); } }); legend { font-weight: bold; } div.message { width: 400px; } div.message div.right fieldset { text-align: left; position: relative; border-color: #6f6; background-color: #dfd; color: #040; } div.message div.right fieldset.to { background-color: #ddf; } div.message div.right fieldset img.del { width: 16px; top: 0; right: 8px; position: absolute; opacity: 0.3; }Ответ 2
Ответ находятся тут. https://javascript.ru/basic/closure#primer-oshibochnogo-ispolzovaniya Причём вступление автора "С вопроса "Почему это не работает?" люди обычно начинают изучение замыканий." Просто, как никогда, попадает в точку и бьёт напролом! Рабочий пример будет выгладить так. window.addEventListener('load',function() { var imgs=document.getElementsByClassName('right')[0].getElementsByClassName('del'); for(var i=0,l=imgs.length;i0&&!isNaN(x.dataset.fid)&&x.dataset.fid>0&&x.dataset.title) alert('Are you sure you want to delete '+x.dataset.title+' message?'); } }(img)); } }); Но всё же, стандарт ECMA Script 2015 позволяет воспользоваться более простым и изящным методом, объявляя переменные не через var, а через let. window.addEventListener('load',function() { let imgs=document.getElementsByClassName('right')[0].getElementsByClassName('del'); for(let i=0,l=imgs.length;i 0&&!isNaN(img.dataset.fid)&&img.dataset.fid>0&&img.dataset.title) alert('Are you sure you want to delete '+img.dataset.title+' message?'); }); } }); Ответ 3
В качестве примера могу привести следующий код: //функция создает объекты в циле, из интерисующих нас параметров box- контейнер в котором находятся DOM эелементы и plate - сами элементы function createPlates(box, plate, step, speed, bool) { var box = document.querySelector(box); if(!box) { return false; } else { var plates = Array.prototype.slice.apply(box.querySelectorAll(plate)); //в цикле создаем объекты с помощью конструктора (будет описан ниже) for(var i = 0; i < plates.length; i++) { plates[i] = new ServisePlate (plates[i], step, speed, bool); } // на контейнер вешаем обработчик на клик box.addEventListener('click', function(event) { // И перебираем массив plates с созданными объектами plates.forEach(function(element, i) { // если объект события совпадает со ссылкой на DOM элемент, сохраненной в объекте, запускаем его метод. if(event.target == plates[i].elem) { plates[i].disclose(); // Здесь проверка на клик по дочернему элементу необходимого нам элемента }else if(plates[i].elem.contains(event.target)){ for(var j = 0; j < plates.length; j++) { if(plates[j].elem.style.height) { plates[j].disclose(); } } plates[i].disclose(); }else { return false; } }); }); } } createPlates('.servises', '.servises_content', 5, 20, true); Собственно сам конструктор: function ServisePlate(plate, step, speed, headSet) { this.elem = plate; this.header = plate.querySelector('h3'); this.body = plate.querySelector('div'); this.link = plate.querySelector('a'); if(this.link) { this.bodySize = this.body.offsetHeight + this.link.offsetHeight; }else{ this.bodySize = this.body.offsetHeight; } this.settings = { step: step, speed: speed } this.initialSize = this.elem.offsetHeight; var self = this; function setHead() { if(29 < self.header.innerHTML.length) { self.header.classList.add('servises_content_header--p18'); } if(59 < self.header.innerHTML.length) { self.header.classList.remove('servises_content_header--p18'); self.header.classList.add('servises_content_header--p1'); } } if(headSet === true) { setHead(); } } И его метод disclose, сохраненный в прототипе: ServisePlate.prototype.disclose = function() { function showBody() { if(this.elem.offsetHeight < (this.bodySize + this.initialSize)) { this.elem.style.height = this.elem.offsetHeight + this.settings.step + 'px'; setTimeout(showBody.bind(this), this.settings.speed); } } function hideBody() { if(this.elem.offsetHeight > this.initialSize) { this.elem.style.height = this.elem.offsetHeight - this.settings.step + 'px'; setTimeout(hideBody.bind(this), this.settings.speed); }else { this.elem.style.height = ''; } } if(this.elem.offsetHeight == this.initialSize || this.elem.offsetHeight >= (this.bodySize + this.initialSize)) { if(this.elem.offsetHeight == this.initialSize) { showBody.call(this); } else { hideBody.call(this); } } } Правда некоторые участки кода у меня самого вызывают подозрения на правильность, так как я уже говорил что мой js тянет на 3 с минусом. Но суть не в правильности тех или иных участков, а в подходе в целом.
Комментариев нет:
Отправить комментарий