217

In JavaScript, what is the best way to remove a function added as an event listener using bind()?

Example

(function(){

    // constructor
    MyClass = function() {
        this.myButton = document.getElementById("myButtonID");
        this.myButton.addEventListener("click", this.clickListener.bind(this));
    };

    MyClass.prototype.clickListener = function(event) {
        console.log(this); // must be MyClass
    };

    // public method
    MyClass.prototype.disableButton = function() {
        this.myButton.removeEventListener("click", ___________);
    };

})();

The only way I can think of is to keep track of every listener added with bind.

Above example with this method:

(function(){

    // constructor
    MyClass = function() {
        this.myButton = document.getElementById("myButtonID");
        this.clickListenerBind = this.clickListener.bind(this);
        this.myButton.addEventListener("click", this.clickListenerBind);
    };

    MyClass.prototype.clickListener = function(event) {
        console.log(this); // must be MyClass
    };

    // public method
    MyClass.prototype.disableButton = function() {
        this.myButton.removeEventListener("click", this.clickListenerBind);
    };

})();

Are there any better ways to do this?

takfuruya
  • 2,453
  • 2
  • 15
  • 10
  • 4
    What you are doing except `this.clickListener = this.clickListener.bind(this);` and `this.myButton.addEventListener("click", this.clickListener);` – Esailija Jul 19 '12 at 16:46
  • That is very nice. This may be a different topic, but it made me wonder whether I should do bind(this) for the rest of my methods that use the "this" keyword even though it would make method calls inefficient. – takfuruya Jul 19 '12 at 17:23
  • I always do this as a first thing in the constructor for all the methods that are going to be passed somewhere, regardless if I am going to remove them later. But not for all methods, just those that are passed around. – Esailija Jul 19 '12 at 17:25
  • What you're doing makes sense. But if this was part of a library, for instance, you can never know which MyClass' methods (documented as being "public") would be passed around. – takfuruya Jul 19 '12 at 18:19
  • Just FYI, the Underscore library has a `bindAll` function that simplifies binding methods. Inside your object initializer you just do `_.bindAll(this)` to set every method in your object to a bound version. Alternatively, if you only want to bind some methods (which I'd recommend, to prevent accidental memory leaks), you can provide them as arguments: `_.bindAll(this, "foo", "bar") // this.baz won't be bound`. – machineghost Jul 19 '12 at 18:25
  • @machineghost, that was a super helpful tip and exactly what I needed. I have a relatively complex architecture and creating new bound functions in order to remove the event listeners would be quite a refactor. Plus, I'm already using Lodash. Thank you. – Andi Apr 07 '16 at 20:37
  • Glad I could help :) – machineghost Apr 07 '16 at 20:52

10 Answers10

347

Although what @machineghost said was true, that events are added and removed the same way, the missing part of the equation was this:

A new function reference is created after .bind() is called.

See Does bind() change the function reference? | How to set permanently?

So, to add or remove it, assign the reference to a variable:

var x = this.myListener.bind(this);
Toolbox.addListener(window, 'scroll', x);
Toolbox.removeListener(window, 'scroll', x);

This works as expected for me.

Ben
  • 54,723
  • 49
  • 178
  • 224
  • 4
    Excellent, this should be the accepted answer. Thanks for updating an old topic, this topic came up on search engine as number one hit and it didn't have a proper solution till you posted this now. – Blargh Apr 16 '14 at 19:24
  • 1
    This is no different from (and no better than) the method mentioned in the question. – Peter Tseng Feb 12 '16 at 02:07
  • I cant understand, how do u make it work with a click event, thanks – Alberto Acuña Feb 25 '16 at 09:38
  • 1
    @AlbertoAcuña Modern browsers use `.addEventListener(type, listener)` and `.removeEventListener(type, listener)` to add and remove events on an element. For both, you can pass the function reference described in the solution as the `listener` parameter, with `"click"` as the type. https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener – Ben Feb 25 '16 at 15:45
  • Thanks for the answer! Really helped me! I got into a slight problem when I tried implementing it. I posted the question here:http://stackoverflow.com/questions/38387130/this-doesnt-work-when-assign-and-bind-to-a-variable I would really appreciate it if you can answer it. – Jessica Jul 15 '16 at 14:27
  • @Jessica apologies, I'm a bit late to the game here. Looks like some others stepped in. Good luck! – Ben Jul 16 '16 at 13:59
  • 2
    this helps me even-though this answer posted 4 years ago :) – user2609021 Feb 24 '18 at 04:37
  • this saved me! thank you for the clear explanation. – jared Oct 26 '19 at 22:57
  • this works!!! that was the best answer so far in the internet, thanks a lot! saved my day – WorldOfEmre Dec 09 '21 at 16:27
  • 1
    make sure that `x` listener is not being replaced (renewed) before calling subsequent `addListener()` or `removeListener()`s. Otherwise, you will lose reference to the previous listeners. – Aryo Dec 13 '21 at 09:25
  • How would this look if you need to pass arguments to the x function ? – juancho Mar 10 '23 at 16:39
  • 1
    @juancho Essentially the same; `this.myListener.bind(this, arg1, arg2)` will create a reference bound with those arguments. – Ben Mar 10 '23 at 16:42
54

For those who have this problem while registering/removing listener of React component to/from Flux store, add the lines below to the constructor of your component:

class App extends React.Component {
  constructor(props){
    super(props);
    // it's a trick! needed in order to overcome the remove event listener
    this.onChange = this.onChange.bind(this);  
  }
  // then as regular...
  componentDidMount (){
    AppStore.addChangeListener(this.onChange);
  }
  
  componentWillUnmount (){
    AppStore.removeChangeListener(this.onChange);
  }

  onChange () {
    let state = AppStore.getState();
    this.setState(state);
  }
  
  render() {
    // ...
  }
  
}
Ben
  • 54,723
  • 49
  • 178
  • 224
Raichman Sergey
  • 684
  • 6
  • 7
  • 10
    Nice trick, but what does React/Flux have to do with anything? – Peter Tseng Feb 12 '16 at 02:17
  • This appears to be the correct approach when adding and removing event listeners from different classes or prototype functions, which is I believe the connection with this also applying to React components/classes. You're binding it at a common (e.g., root) instance level. – Keith DC Jan 21 '18 at 02:31
  • 1
    `this.onChange = this.onChange.bind(this)` actually this is what I was looking for. The function bound on `this` for ever :) – Paweł Mar 21 '18 at 01:55
2

It doesn't matter whether you use a bound function or not; you remove it the same way as any other event handler. If your issue is that the bound version is its own unique function, you can either keep track of the bound versions, or use the removeEventListener signature that doesn't take a specific handler (although of course that will remove other event handlers of the same type).

(As a side note, addEventListener doesn't work in all browsers; you really should use a library like jQuery to do your event hook-ups in a cross-browser way for you. Also, jQuery has the concept of namespaced events, which allow you to bind to "click.foo"; when you want to remove the event you can tell jQuery "remove all foo events" without having to know the specific handler or removing other handlers.)

alex
  • 479,566
  • 201
  • 878
  • 984
machineghost
  • 33,529
  • 30
  • 159
  • 234
  • I'm aware of the IE issue. I'm developing an application that relies heavily on canvas so IE7- are out. IE8 supports canvas but at the minimum. IE9+ supports addEventListener. jQuery's Namespaced Events looks very neat. The only thing I'm worried about is efficiency. – takfuruya Jul 19 '12 at 17:37
  • The jQuery folks work *very* hard to keep their library performing well, so I wouldn't worry about that too much. However, given your strict browser requirements you might want to check out Zepto instead. It's sort of like a scaled-down version of jQuery that is faster but can't support older browsers (and has some other limits). – machineghost Jul 19 '12 at 18:28
  • JQuery namespaced events are widely used and have virtually no performance concerns. Telling someone not to use a tool that will make their code easier and (arguably more importantly) easier to understand, would be horrible advice, especially if done so out of an irrational fear of JQuery and imaginary performance concerns. – machineghost Sep 03 '13 at 17:25
  • 1
    Which signature would that be? [The MDN page on removeEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener) shows that both of the first two arguments are required. – Coderer Aug 02 '18 at 12:27
  • My mistake. It's been years since I wrote that answer, but I must have been thinking of jQuery's `off` or `unbind` method. To remove all listeners on an element you have to keep track of them as they get added (which is something jQuery or other libraries can do for you). – machineghost Aug 02 '18 at 22:01
1

jQuery solution:

let object = new ClassName();
let $elem = $('selector');

$elem.on('click', $.proxy(object.method, object));

$elem.off('click', $.proxy(object.method, object));
Eduard Kolosovskyi
  • 1,396
  • 12
  • 18
1

We had this problem with a library we could not change. Office Fabric UI, which meant we could not change the way event handlers were added. The way we solved it was to overwrite the addEventListener on the EventTarget prototype.

This will add a new function on objects element.removeAllEventListers("click")

(original post: Remove Click handler from fabric dialog overlay)

        <script>
            (function () {
                "use strict";

                var f = EventTarget.prototype.addEventListener;

                EventTarget.prototype.addEventListener = function (type, fn, capture) {
                    this.f = f;
                    this._eventHandlers = this._eventHandlers || {};
                    this._eventHandlers[type] = this._eventHandlers[type] || [];
                    this._eventHandlers[type].push([fn, capture]);
                    this.f(type, fn, capture);
                }

                EventTarget.prototype.removeAllEventListeners = function (type) {
                    this._eventHandlers = this._eventHandlers || {};
                    if (type in this._eventHandlers) {
                        var eventHandlers = this._eventHandlers[type];
                        for (var i = eventHandlers.length; i--;) {
                            var handler = eventHandlers[i];
                            this.removeEventListener(type, handler[0], handler[1]);
                        }
                    }
                }

                EventTarget.prototype.getAllEventListeners = function (type) {
                    this._eventHandlers = this._eventHandlers || {};
                    this._eventHandlers[type] = this._eventHandlers[type] || [];
                    return this._eventHandlers[type];
                }

            })();
        </script>
Peter
  • 745
  • 6
  • 17
0

Here is the solution:

var o = {
  list: [1, 2, 3, 4],
  add: function () {
    var b = document.getElementsByTagName('body')[0];
    b.addEventListener('click', this._onClick());

  },
  remove: function () {
    var b = document.getElementsByTagName('body')[0];
    b.removeEventListener('click', this._onClick());
  },
  _onClick: function () {
    this.clickFn = this.clickFn || this._showLog.bind(this);
    return this.clickFn;
  },
  _showLog: function (e) {
    console.log('click', this.list, e);
  }
};


// Example to test the solution
o.add();

setTimeout(function () {
  console.log('setTimeout');
  o.remove();
}, 5000);
Nazar Vynnytskyi
  • 4,647
  • 2
  • 21
  • 22
0

As others have said, bind creates a new function instance and thus the event listener cannot be removed unless it is recorded in some way.

For a more beautiful code style, you can make the method function a lazy getter so that it's automatically replaced with the bound version when accessed for the first time:

class MyClass {
  activate() {
    window.addEventListener('click', this.onClick);
  }

  deactivate() {
    window.removeEventListener('click', this.onClick);
  }

  get onClick() {
    const func = (event) => {
      console.log('click', event, this);
    };
    Object.defineProperty(this, 'onClick', {value: func});
    return func;
  }
}

If ES6 arrow function is not supported, use const func = (function(event){...}).bind(this) instead of const func = (event) => {...}.

Raichman Sergey's approach is also good, especially for classes. The advantage of this approach is that it's more self-complete and has no separated code other where. It also works for an object which doesn't have a constructor or initiator.

Danny Lin
  • 2,050
  • 1
  • 20
  • 34
-1

If you want to use 'onclick', as suggested above, you could try this:

(function(){
    var singleton = {};

    singleton = new function() {
        this.myButton = document.getElementById("myButtonID");

        this.myButton.onclick = function() {
            singleton.clickListener();
        };
    }

    singleton.clickListener = function() {
        console.log(this); // I also know who I am
    };

    // public function
    singleton.disableButton = function() {
        this.myButton.onclick = "";
    };
})();

I hope it helps.

Diogo Schneider
  • 339
  • 2
  • 9
-1

can use about ES7:

class App extends React.Component {
  constructor(props){
    super(props);
  }
  componentDidMount (){
    AppStore.addChangeListener(this.onChange);
  }

  componentWillUnmount (){
    AppStore.removeChangeListener(this.onChange);
  }

  onChange = () => {
    let state = AppStore.getState();
    this.setState(state);
  }

  render() {
    // ...
  }

}
chiic
  • 1
-2

It's been awhile but MDN has a super explanation on this. That helped me more than the stuff here.

MDN :: EventTarget.addEventListener - The value of "this" within the handler

It gives a great alternative to the handleEvent function.

This is an example with and without bind:

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
}

A problem in the example above is that you cannot remove the listener with bind. Another solution is using a special function called handleEvent to catch any events:

Noitidart
  • 35,443
  • 37
  • 154
  • 323