Как передать саму функцию в bind JavaScript

200
01 мая 2018, 00:50

Всем привет!
Как и все, кто волею судьбы и руководства изучает JavaScript, столкнулся с передачей параметров в addEventListener.
Проблема легко решается с помощью .bind:

handler = function(contextParameter){
console.log(contextParameter);
}
var f = handler.bind(this, context);
parentDiv.addEventListener('click', f('Hello Wolrd!'));

Однако возникает другая проблема, когда создаёшь в цикле несколько объектов и хочешь удалить этот Listener.
Не могу сослаться внутри handler на саму функцию внутри removeEventListener, чтобы удалить её на(...)конец.
По-быстрому придумал костыли - заношу созданные переменные в массив a_f, и в .bind передаю текущий i
Далее, удаляю таким макаром:
o_parentToPass.removeEventListener('click', a_f[v_na]);
Где v_na - параметр, передаваемый в handler, соответствующий i
Помогите, плиз, как сделать всё красиво! Перелистал уже всю мозилу и w3, ничего подходящего не могу найти(((
Как мне передать саму переменную f внутри f = handler.bind(this, o_context, i);
По подобию:
f = handler.bind(this, o_context, this);
Или f = handler.bind(this, o_context, f);
Эти два варианта сверху не работают (

Вот код:

var a_f = [];
var handler = function(o_parentToPass, v_na){
if(o_parentToPass.classList.contains('test-rotate')){
    o_parentToPass.classList.remove('test-rotate');
}else{
    o_parentToPass.classList.add('test-rotate');
}
o_parentToPass.removeEventListener('click', a_f[v_na]);
console.log(v_na);
}
var f_createDiv = function(){
for(var i = 0; i < 3; i++){
    var o_parent = document.createElement('div');
    o_parent.classList.add('parent');
    var o_picture = document.createElement('div');
    o_picture.classList.add('child', 'picture');
    var o_cover = document.createElement('div');
    o_cover.classList.add('child', 'cover', 'rotate-cover');
    var vs_text = document.createElement('p');
    vs_text.innerHTML = i;
    o_parent.appendChild(o_picture);
    o_parent.appendChild(o_cover);
    o_parent.appendChild(vs_text);
    var o_context = o_parent;
    var f = handler.bind(this, o_context, i);
    a_f.push(f);
    o_parent.addEventListener('click', f);
    document.body.appendChild(o_parent);
}
}

Вариант с

for (var i=0; i<3; i++) {
const node = ...;
const handler = function() {
    // ...
    node.removeEventListener("click", handler);
}
node.addEventListener("click", handler);
}

Не совсем подходит. Т.к. handler в моём случае - внешний. Его нельзя разместить внутри цикла.

Короче - лучший вариант "прикрутить" к вызываемому методу добавление listener с помощью calm передавать ноду через bind и добавлять listener уже в самом методе.

Answer 1

Самый простой вариант - через замыкание. Просто создаем переменную и на нее ссылаемся:

for (var i=0; i<3; i++) {
    const node = ...;
    const handler = function() {
        // ...
        node.removeEventListener("click", handler);
    }
    node.addEventListener("click", handler);
}

Если вы пишите для браузеров которые не поддерживают let и const - то можно использовать еще одно замыкание чтобы разделить области видимости:

for (var i=0; i<3; i++) 
    !function(){
        var node = ...;
        var handler = function() {
            // ...
            node.removeEventListener("click", handler);
        }
        node.addEventListener("click", handler);
    }();

Здесь я использовал IIFE (Immediately Invoked Function Expression) - немедленно вызываемое функциональное выражение. Этот трюк позволяет создать на каждой итерации цикла новые переменные node и handler, без него бы метод removeEventListener всегда отписывал последний обработчик события а не текущий.

Если прошлый пример кажется слишком магическим - вот то же самое, но без IIFE:

for (var i=0; i<3; i++)
    step(i);
function step(i) {
    var node = ...;
    var handler = function() {
        // ...
        node.removeEventListener("click", handler);
    }
    node.addEventListener("click", handler);        
}
Answer 2

Вариант f = handler.bind(this, o_context, f) не работает, потому что сначала выполняется правая часть выражения. А значит в bind передается старое значение f (или undefined на первой итерации цикла).

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

function handler(element, parameter, getEventHandler) { 
  element.innerHTML += parameter; 
  if (true) // ваше условие 
    element.removeEventListener('click', getEventHandler()); 
} 
 
for (var i = 0; i < 3; i++) { 
  var div = document.createElement('div'); 
  div.innerHTML = i; 
   
  (function () { 
    var bindedHandler; 
    var getBindedHandler = function () { return bindedHandler; } 
    bindedHandler = handler.bind(this, div, '-clicked-' + i, getBindedHandler); 
    div.addEventListener('click', bindedHandler); 
  })(); 
   
  document.body.append(div); 
}

Если задача требудет выполнения обработчика только 1 раз, можно испольвазть третий аргумент addEventListener. Мы указываем браузеру, что обработчик должен вызваться только 1 раз, а потом браузер сам удалит его:

for (var i = 0; i < 3; i++) { 
  var div = document.createElement('div'); 
  div.innerHTML = i; 
   
  div.addEventListener('click', function () { 
    this.innerHTML += '-clicked'; 
  }, { once: true }); 
   
  document.body.append(div); 
}

Answer 3

TL;DR:
Достаточно сделать так:

<element>.addEventListener(<eventName>, { handleEvent: <handlerFunction> }); 

, без всякого излишнего секса с bind() или замыканием функции.

Пояснение:
Аргументом в addEventListener можно передавать не только функцию, но и объект соответствующий интерфейсу EventListener, насовав все необходимые Вам параметры в его (объекта) поля. Внутри функции-листенера, этот объект будет доступен через this.

Пример:
Сниппет ниже, демонстрирует создание в цикле элементов, реагирующих на клик увеличением их значения на рандомную величину от 1 до 25 включительно. Как только значение достигает 50, слушатель события удаляется.
Чтобы было нагляднее, каждый добавляемый объект слушателя содержит три поля:

  • handleEvent - непосредственно обработчик события. В данном случае, это обычная именованная функция из глобальной области видимости;

  • el - элемент, владеющий слушателем события (можно не передавать, так как в обработчике доступно свойство target аргумента Event. То есть, вместо this.el можно юзать evt.target);

  • rndIncr - референс функции, генерирующей случайную величину инкремента.

for (let elem, evtHandler, i = 0; i < 10; i++) { 
  elem = document.createElement('span');  
  elem.classList.add('test');  
  elem.textContent = '0';  
  evtHandler = { 
    el: elem,  
    rndIncr: rndIncrement, 
    handleEvent: clickHandler  
  };  
  elem.addEventListener('click', evtHandler);  
  document.body.appendChild(elem);  
} 
 
function clickHandler(evt) { 
  let i = this.rndIncr(),  
      newVal = +this.el.textContent + i;  
  console.clear();  
  console.log('Случайный инкремент: ' + i);  
  this.el.textContent = String(newVal); 
  if (newVal >= 50) { 
    this.el.style.boxShadow = '0 0 0 1px #ccc';  
    this.el.removeEventListener('click', this);  
  } 
} 
 
function rndIncrement() { 
  return Math.floor(Math.random() * 25) + 1;  
}
.test { 
  display: inline-block;  
  margin: 5px;  
  width: 50px; line-height: 50px;  
  text-align: center;  
  user-select: none;  
  box-shadow: 0 2px 5px 0 #0004;  
  transition: box-shadow 0.3s ease-out;  
}

Почему это важно, и чем это (имхо) лучше:

Во-первых, такой "объектный" подход читабельнее по коду:
«вот объект с такими-то полями, и вот мы его передаем»
вместо варианта ф. выражений:
«вот функция, которая создает замыкание, в котором объявлены переменные, которые передаются вложенной функции , в доме который построил Джек»
То есть, решение типа "есть же callback hell, так давайте еще мини function expression hell запилим" - скажем так, сомнительное... Плюс, не будем забывать что вызовы в JavaScript дорогие.

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

В-третьих, такой подход "более стандартный", что ли... В том смысле, что метод addEventListener явно предполагает использование объектов во втором аргументе - тогда как использование bind() или вложенных функций, в данном контексте, смахивает на костыль.

READ ALSO
VueJS Разница между Methods и Computed

VueJS Разница между Methods и Computed

Недавно начал учить Vue и сразу-же застрял на таком моменте

205
Изменения при клике JS

Изменения при клике JS

Мне надо проверить были ли изменения при клике в iteminfo0_item_name или в iteminfo1_item_name

205
Не могу подключить файл к скрипту

Не могу подключить файл к скрипту

Есть два файла wtphp и api

202
Удаление данных из таблицы после теста

Удаление данных из таблицы после теста

Как в phpunit, можно очистить таблицу в тестовой базе после каждого теста? Пытался использовать:lluminate\Foundation\Testing\RefreshDatabase (примочка для тестирования...

207