Correct way to bind "this" in JavaScript event callbacks?
Solution 1
If you add a namespace to your events you can bind events and easily unbind them all at once.
To bind:
$(this.element).on('keypress.mynamespace', this.searchKeyPressed.bind(this));
To unbind:
$(this.element).off('.mynamespace');
Solution 2
First, unless you expect your page to be very long lived (or you're listening to keypresses with hundreds of things, which is a very big architecture problem), this isn't going to be much of an issue, where memory is concerned, even if phones had "keys".
Second, .bind
has great support for all browsers less than half a decade old, and is dirt simple to polyfill.
Third: you're 100% right, that it's not cool to have to cache the function to be able to deal with it later, so let's do something about that.
There's a little known trick to addEventListener
(and attachEvent
), in that it happily supports objects, with handleEvent
methods on them.
I don't use this for everything, as sometimes it's really just not worth it, but for game-making, I've used it for inputs, kind of like so:
class Input {
constructor (events) {
this.events = events || [];
}
handleEvent (e) {
var input = this;
var method = e.type;
if (typeof input[method] === "function") {
input.dispatchEvent(method, e);
}
}
dispatchEvent (method, content) {
var input = this;
input[method](content);
}
listen (el, events) {
var input = this;
events = events || input.events;
events.forEach(event => el.addEventListener(event, input));
return this;
}
ignore (el, events) {
var input = this;
events = events || input.events;
events.forEach(event => el.removeEventListener(event, input));
return this;
}
}
class Keyboard extends Input {
constructor () {
super(["keydown", "keyup"]);
var keyboard = this;
keyboard.keys = new Set();
}
press (key) { this.keys.add(key); }
release (key) { this.keys.delete(key); }
isPressed (key) { return this.keys.has(key); }
keydown (e) {
var key = e.keyCode;
this.press(key);
}
keyup (e) {
var key = e.keyCode;
this.release(key);
}
}
I could then:
var gameplayEvents = ["keyup", "keydown"];
var keyboard = new Keyboard();
keyboard.listen(canvas, gameplayEvents);
// ongameover
keyboard.ignore(canvas, gameplayEvents);
And if you'll note, it's all 100% pure JS.
No jQuery, extJS, etc.
And really, it's seriously not a lot more code, either.
I could make it one object-literal, if I just needed one instance to handle mouseup and mousedown; really all I need is an object with a handleEvent, to become this
inside of the handleEvent callback.
There's only one instance to worry about. I don't cache anything extra, if I need to unregister.
jQuery (and others) actually use this internally, to optimize the atrocious code which they're typically abused into producing.
Yes, perhaps I'm cheating by using ES6... ...but it's not necessary at all.
It's just more cheerful than what I used to do:
function Parent (args) { }
extend(Parent.prototype, { /*public-methods*/ });
function Child (args) {
Parent.call(this);
// construct
}
extend(
Child.prototype,
Parent.prototype,
{ /*public-override-methods*/ },
{ constructor: Child }
);
And again, there are lots of times when bind is 100% valid.
There's a proposal right now, for an ES7 version of bind, which would potentially produce the same value, every time it's called (if it goes through that way).
With the added benefit that the syntax allows for chaining all kinds of awesome things together, as well.
Solution 3
Instead of using bind
, you could use the jQuery.proxy
function to preserve context, which creates a wrapper function you can unbind:
jQuery.proxy
has multiple variants, but for now the jQuery.proxy(function, context)
is what you'll have to use:
p.registerListeners = function () {
$(this.element).on('keypress', $.proxy(this.searchKeyPressed, this));
};
p.unregisterListeners = function () {
$(this.element).off('keypress', $.proxy(this.searchKeyPressed, this));
};
Solution 4
Add bind(this)
in the constructor - so you dont create new function every time you call .bind
. bind
creates new function every time you call it, so if you attach it with this: $el.on("click", this.handler.bind(this))
, you cannot detach it with $el.off("click", this.handler.bind(this))
because the handler is not the same. (this.handler.bind(this) !== this.handler.bind(this)
) If you save that reference to that binded function (like in the constructor this.handler = this.handler.bind(this)
), then you can $el.on("click", this.handler)
and $el.off("click", this.handler)
, because the handler is the same.
With this method, you are essentially overwriting that function for that instance. It will no longer call function like on prototype, but function like on that instnce.
function MyObject($el) {
this.testValue = "I am really this!";
this.$el = $el;
this.onClick = this.onClick.bind(this);
this.render();
}
MyObject.prototype.onClick = function(evt) {
console.log(this.testValue); // logs "I am really this!"
}
MyObject.prototype.render = function() {
var $a = $("<a>", {"text": "Click on me!"}).appendTo($el.empty());
$a.on("click", this.onClick);
}
Triynko
Updated on June 27, 2022Comments
-
Triynko almost 2 years
I created a class called SearchBox to handle search interaction (delayed trigger, search on enter key press, preventing searches while one is active, synchronizing results when a search completes and the text has changed, etc.).
All the class methods are prototype methods, meant to be accessed via
this
. In the following code, assumep
is the class's prototype.p.registerListeners = function () { $(this.element).on('keypress', this.searchKeyPressed); }; p.unregisterListeners = function () { $(this.element).off('keypress', this.searchKeyPressed); };
That doesn't work, because when the keypress event calls the
searchKeyPressed
handler, it doesn't do so in the context ofthis
. The only solution I can come up with is one that only modern browsers support, which is to bind the callback tothis
, which actually creates a new function. Since it creates a new function, I have to cache it in order to remove it later, since I have to pass the same reference tooff
that I passed toon
.Is there a better way to do it than this, or is this ok?
var boundKeyPressed; p.registerListeners = function () { boundKeyPressed = this.searchKeyPressed.bind(this); $(this.element).on('keypress', boundKeyPressed); }; p.unregisterListeners = function () { $(this.element).off('keypress', boundKeyPressed); };
I thought that maybe
jQuery.on
would provide a way to do this event binding automatically, but instead it seems like it bindsthis
to different things depending on how it's called. For example, when usingon('eventname',instance.func)
,this
is the "currentTarget" (not necessarily "target" in bubbling terms), whereas when usingon('eventname','selector',instance.func)
,this
refers to the element matching the selector. In either case, thefunc
runs as though it has no relationship withinstance
. -
Triynko over 8 yearsA closure is no different than 'bind'. It's creating a new function, and it's doing it for every instance. That defeats the memory-saving purpose of using prototype methods, which are created only once and shared by all instances of the class.
-
Comptonburger over 8 yearsI thought the main concern was browser support. Using a closure rather than bind is at least supported by all browsers.
-
Triynko over 8 yearsThat's part of the issue, but the other part is the redundancy of always having to bind a function explicitly, when it's already assigned to a prototype.
this.method.bind(this)
andme.method.apply(me)
are both doing the same basic thing, although the latter has better support. -
Mike Cluck over 8 yearsThat's not really what OP was asking.
-
Koen. over 8 yearsMaybe not, but this circumvents his problem of the context of an event handler
-
Triynko over 8 yearsThis is actually useful. So I'm still doing the bind, but I don't have to keep an external variable. This must be a feature of jQuery where it keeps event handlers organized into namespaces, so you can remove named subsets of them (without having to maintain references to them) or being forced to remove all handlers. This is probably also less prone to memory leaks, since jQuery is managing the references. In AS3, I never had to worry, because I would call addEventListener with the weak reference parameter set to true.
-
Comptonburger over 8 yearsjQuery has no notion of the object. It only associates the handler with $(this.element) and 'keypress'. Unfortunately jQuery.on doesn't let you specify a scope. I don't think what you want is possible.
-
Koen. over 8 years@Triynko but are you sure that bind creates s new function instead of just setting the context? The polyfill obviously does, but the native method I don't know..
-
Triynko over 8 yearsYeah, the closest thing to specifying a scope is using 'bind'. I thought maybe jQuery had a way of specifying scope, but the behavior seems to be baked in to how you call 'on', with no ability to override that behavior. Really, we should be using 'target', 'currentTarget', and in jQuery's case for delegated events, something like 'selectedTarget', rather than arbitrarily altering what
this
refers to. In any case, I'm going to see if anyone has any other clever ideas. Both of these answers are helpful. -
Comptonburger over 8 yearsSeems like quite a failure on jQuery's part. I haven't worked with jQuery in some time, I work with ExtJS at the moment - and it allows you to specify the scope whenever you are setting an event handler. Can't imagine not being able to.
-
Norguard over 8 years@Koen. Yes.
.call
and.apply
set the context..bind
creates a wrapping closure around that context-swap.function sum (a, b, c) { return a + b + c; } var boundSum = sum.bind(window, 1, 2); sum === boundSum; /* false */ boundSum(3); /* 6 */