Значення “this” у обробнику за допомогою addEventListener


77

Я створив об’єкт Javascript за допомогою прототипування. Я намагаюся відображати таблицю динамічно. Хоча частина візуалізації проста і працює нормально, мені також потрібно обробляти певні події на стороні клієнта для таблиці, що динамічно відображається. Це теж легко. У мене виникають проблеми з посиланням "this" всередині функції, яка обробляє подію. Замість "this" посилається на об'єкт, він посилається на елемент, що викликав подію.

Див. Код. Проблемна область полягає в ticketTable.prototype.handleCellClick = function():

function ticketTable(ticks)
{
    // tickets is an array
    this.tickets = ticks;
} 

ticketTable.prototype.render = function(element)
    {
        var tbl = document.createElement("table");
        for ( var i = 0; i < this.tickets.length; i++ )
        {
            // create row and cells
            var row = document.createElement("tr");
            var cell1 = document.createElement("td");
            var cell2 = document.createElement("td");

            // add text to the cells
            cell1.appendChild(document.createTextNode(i));
            cell2.appendChild(document.createTextNode(this.tickets[i]));

            // handle clicks to the first cell.
            // FYI, this only works in FF, need a little more code for IE
            cell1.addEventListener("click", this.handleCellClick, false);

            // add cells to row
            row.appendChild(cell1);
            row.appendChild(cell2);


            // add row to table
            tbl.appendChild(row);            
        }

        // Add table to the page
        element.appendChild(tbl);
    }

    ticketTable.prototype.handleCellClick = function()
    {
        // PROBLEM!!!  in the context of this function, 
        // when used to handle an event, 
        // "this" is the element that triggered the event.

        // this works fine
        alert(this.innerHTML);

        // this does not.  I can't seem to figure out the syntax to access the array in the object.
        alert(this.tickets.length);
    }

Відповіді:


46

Вам потрібно "прив'язати" обробник до вашого екземпляра.

var _this = this;
function onClickBound(e) {
  _this.handleCellClick.call(cell1, e || window.event);
}
if (cell1.addEventListener) {
  cell1.addEventListener("click", onClickBound, false);
}
else if (cell1.attachEvent) {
  cell1.attachEvent("onclick", onClickBound);
}

Зверніть увагу, що обробник подій тут нормалізує eventоб'єкт (передається як перший аргумент) і викликає handleCellClickу відповідному контексті (тобто посилаючись на елемент, до якого було приєднано слухач події).

Також зауважте, що нормалізація контексту тут (тобто встановлення належного thisв обробнику подій) створює циклічне посилання між функцією, яка використовується як обробник подій ( onClickBound), та об’єктом елемента ( cell1). У деяких версіях IE (6 і 7) це може, і, можливо, призведе до витоку пам'яті. По суті, це витік - це те, що браузер не звільняє пам’ять при оновленні сторінки через кругові посилання, що існують між рідним та об’єктом хосту.

Щоб його обійти, вам потрібно було б: а) відмовитись від thisнормалізації; б) застосовувати альтернативну (і більш складну) стратегію нормалізації; в) «очистити» існуючі обробники подій на сторінці вивантаження, тобто за допомогою removeEventListener, detachEventі елементи nullІнг (які , до жаль , зробило б швидко історія навігації браузерів марно).

Ви також можете знайти бібліотеку JS, яка подбає про це. Більшість із них (наприклад: jQuery, Prototype.js, YUI тощо) зазвичай обробляють очищення, як описано в (c).


Звідки var _this = this; в контексті мого коду йти? Чи потрібно додавати onClickBound (e) до прототипу?
Darthg8r

В render, безпосередньо перед приєднанням прослуховувача подій. Ви можете просто замінити addEventListenerрядок з оригінального прикладу цим фрагментом.
kangax

Цікаво, що ви згадали про прибирання. Я також фактично знищуватиму ці об’єкти. Спочатку я планував просто зробити .innerHTML = ""; Я думаю, це погано в цьому контексті. Як мені знищити ці таблиці та уникнути згаданого витоку?
Darthg8r

Як я вже говорив раніше, вивчіть removeEventListener/ detachEventі порушуйте кругові посилання. Ось гарне пояснення витоку - jibbering.com/faq/faq_notes/closures.html#clMem
kangax

4
Не знаю чому, але це Я = ці фокуси мені завжди здаються неправильними.
gagarine

86

Ви можете використовувати прив'язку, яка дозволяє вказати значення, яке слід використовувати як це для всіх викликів даної функції.

   var Something = function(element) {
      this.name = 'Something Good';
      this.onclick1 = function(event) {
        console.log(this.name); // undefined, as this is the element
      };
      this.onclick2 = function(event) {
        console.log(this.name); // 'Something Good', as this is the binded Something object
      };
      element.addEventListener('click', this.onclick1, false);
      element.addEventListener('click', this.onclick2.bind(this), false); // Trick
    }

Проблема у наведеному вище прикладі полягає в тому, що ви не можете видалити слухач за допомогою прив’язки. Іншим рішенням є використання спеціальної функції, яка називається handleEvent, щоб вловити будь-які події:

var Something = function(element) {
  this.name = 'Something Good';
  this.handleEvent = function(event) {
    console.log(this.name); // 'Something Good', as this is the Something object
    switch(event.type) {
      case 'click':
        // some code here...
        break;
      case 'dblclick':
        // some code here...
        break;
    }
  };

  // Note that the listeners in this case are this, not this.handleEvent
  element.addEventListener('click', this, false);
  element.addEventListener('dblclick', this, false);

  // You can properly remove the listners
  element.removeEventListener('click', this, false);
  element.removeEventListener('dblclick', this, false);
}

Як завжди mdn - це найкраще :). Я просто копіюю вставлену частину, ніж відповідаю на це питання.


10

Крім того, ще одним способом є використання інтерфейсу EventListener (від DOM2 !!. Цікаво, чому ніхто про це не згадав, вважаючи, що це найоптимальніший спосіб і призначений саме для такої ситуації).

Тобто, замість передачі функції зворотного виклику, ви передаєте об’єкт, який реалізує інтерфейс EventListener. Простіше кажучи, це просто означає, що ви повинні мати властивість в об’єкті, що називається „handleEvent”, яка вказує на функцію обробника подій. Головна відмінність тут полягає в тому, що всередині функції, thisбуде посилатися на об'єкт, переданий в addEventListener. Тобто, this.theTicketTableбуде екземпляром об’єкта в нижчому коді. Щоб зрозуміти, що я маю на увазі, уважно подивіться на змінений код:

ticketTable.prototype.render = function(element) {
...
var self = this;

/*
 * Notice that Instead of a function, we pass an object. 
 * It has "handleEvent" property/key. You can add other
 * objects inside the object. The whole object will become
 * "this" when the function gets called. 
 */

cell1.addEventListener('click', {
                                 handleEvent:this.handleCellClick,                  
                                 theTicketTable:this
                                 }, false);
...
};

// note the "event" parameter added.
ticketTable.prototype.handleCellClick = function(event)
{ 

    /*
     * "this" does not always refer to the event target element. 
     * It is a bad practice to use 'this' to refer to event targets 
     * inside event handlers. Always use event.target or some property
     * from 'event' object passed as parameter by the DOM engine.
     */
    alert(event.target.innerHTML);

    // "this" now points to the object we passed to addEventListener. So:

    alert(this.theTicketTable.tickets.length);
}

Начебто акуратний, але, здається, removeEventListener()з цим не можна ?
knutole

3
@knutole, так ти можеш. Просто збережіть об’єкт у змінну та передайте змінну в addEventListener. Ви можете подивитися тут для довідки: stackoverflow.com/a/15819593/2816199 .
tomekwi

Схоже, TypeScript не любить цей синтаксис, тому він не компілює виклик до addEventListenerоб'єкта як зворотного виклику. Чорт!
David R Tribble

@DavidRTribble Чи намагалися ви призначити об'єкт змінній, а потім передати змінну? Я особисто не пробував TypeScript, але якщо це саме проблема синтаксису, а не те, що тип проблеми "функція не приймає параметр", то це може бути рішенням
kamathln

7

Цей синтаксис стрілки працює для мене:

document.addEventListener('click', (event) => {
  // do stuff with event
  // do stuff with this 
});

це буде батьківський контекст, а не контекст документа


6

З ES6 ви можете використовувати функцію стрілки, оскільки вона використовуватиме лексичний масштаб [0], що дозволяє уникнути необхідності використовувати bindабо self = this:

var something = function(element) {
  this.name = 'Something Good';
  this.onclick1 = function(event) {
    console.log(this.name); // 'Something Good'
  };
  element.addEventListener('click', () => this.onclick1());
}

[0] https://medium.freecodecamp.org/learn-es6-the-dope-way-part-ii-arrow-functions-and-the-this-keyword-381ac7a32881


5

Я знаю, що це старіша публікація, але ви також можете просто призначити контекст змінній self, передати свою функцію в анонімну функцію, яка викликає вашу функцію .call(self)та передає її в контекст.

ticketTable.prototype.render = function(element) {
...
    var self = this;
    cell1.addEventListener('click', function(evt) { self.handleCellClick.call(self, evt) }, false);
...
};

Це працює краще, ніж "прийнята відповідь", тому що контексту не потрібно призначати змінну для всього класу або глобального, скоріше, він акуратно схований у тому самому методі, який прослуховує подію.


2
З ES5 ви отримаєте те ж саме , використовуючи тільки: cell1.addEventListener('click', this.handleCellClick.bind(this));. Залиште останній аргумент, falseякщо ви хочете порівняти з FF <= 5.
tomekwi

Проблема з цим полягає в тому, що якщо у вас є інші функції, викликані від обробника, вам потрібно передати себе вниз по лінії.
mkey

Це рішення я використав. Я б віддав перевагу використанню .bind()методу, але .call()замість цього довелося б використовувати його, оскільки наш додаток ще деякий час повинен підтримувати IE8.
David R Tribble

1
Раніше я використовував self, поки не знайшов selfте саме window, що зараз використовую me.
Цянь Чень,

1
Як ти використовуєш removeEventListenerцей спосіб?
Tamb

1

Під сильним впливом відповіді kamathln та гагарину я думав, що можу вирішити це питання.

Я думав, ви, мабуть, зможете отримати трохи більше свободи, якщо покладете handeCellClick у список зворотних викликів і використаєте об’єкт, що використовує інтерфейс EventListener у події, щоб викликати методи списку зворотних викликів із правильним цим.

function ticketTable(ticks)
    {
        // tickets is an array
        this.tickets = ticks;
        // the callback array of methods to be run when
        // event is triggered
        this._callbacks = {handleCellClick:[this._handleCellClick]};
        // assigned eventListenerInterface to one of this
        // objects properties
        this.handleCellClick = new eventListenerInterface(this,'handleCellClick');
    } 

//set when eventListenerInterface is instantiated
function eventListenerInterface(parent, callback_type) 
    {
        this.parent = parent;
        this.callback_type = callback_type;
    }

//run when event is triggered
eventListenerInterface.prototype.handleEvent(evt)
    {
        for ( var i = 0; i < this.parent._callbacks[this.callback_type].length; i++ ) {
            //run the callback method here, with this.parent as
            //this and evt as the first argument to the method
            this.parent._callbacks[this.callback_type][i].call(this.parent, evt);
        }
    }

ticketTable.prototype.render = function(element)
    {
       /* your code*/ 
        {
            /* your code*/

            //the way the event is attached looks the same
            cell1.addEventListener("click", this.handleCellClick, false);

            /* your code*/     
        }
        /* your code*/  
    }

//handleCellClick renamed to _handleCellClick
//and added evt attribute
ticketTable.prototype._handleCellClick = function(evt)
    {
        // this shouldn't work
        alert(this.innerHTML);
        // this however might work
        alert(evt.target.innerHTML);

        // this should work
        alert(this.tickets.length);
    }

0

Що стосовно

...
    cell1.addEventListener("click", this.handleCellClick.bind(this));
...

ticketTable.prototype.handleCellClick = function(e)
    {
        alert(e.currentTarget.innerHTML);
        alert(this.tickets.length);
    }

e.currentTarget вказує на ціль, яка прив'язана до "події кліку" (до елемента, що викликав подію), поки

bind (це) зберігає значення зовнішнього обсягу thisвсередині функції події кліку.

Якщо ви хочете отримати точну ціль, натисніть натомість e.target .

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.