Javascript scope addEventListener and this
Solution 1
A few things:
Most people will suggest something like
var self = this
because it's fast and easy.But
var self = this
does not separate the view object entirely from the view logic, which coming from a more formal C# background and looking at your code, sounds like something you want to do.In order to have the callback execute only when the event fires, wrap the handler in a function, so that it's evaluated right away, but only executed when and if a
keydown
event fires (see the code below).Understanding scope in JS: Whatever the execution context is, is also the current scope. Your listener was added in a method (called
listen
) onKeyboard.prototype
, but thekeydown
event is actually fired onwindow
-- the handler is executing in a different context than where it was defined; it's executing within the context of what is invoking it, in this case,window
, so it's scoped towindow
unless you bind it to another object viabind
orapply
when it's defined.
In your code, window
is the view a user's interacting with, and Keyboard
is that view's controller. In MVC patterns like what you're probably used to in C#/.NET, views don't tell themselves what to do when things happen, controllers tell views what to do. So, if you were to assign a reference to the controller by using var self = this
like so many do, the view would be managing itself -- but only for that specific handler for keydown
events. This is inconsistent and would become hard to manage in a large project.
A solution:
Keyboard.prototype.listen = function() {
window.addEventListener('keydown', function(e) {
this.handle_keydown(e);
}.bind(this), false);
}
A better solution:
Keyboard.prototype.view = window;
Keyboard.prototype.listen = function() {
this.view.addEventListener('keydown', function(e) {
this.handle_keydown(e);
}.bind(this), false);
}
The best solution (until ES6 class
is ready):
// define
function addViewController(view) {
function ViewController() {
this.handle_keydown = function(args) {
// handle keydown events
};
this.listen = function() {
this.view.addEventListener('keydown', function(e) {
this.handle_keydown(e);
}.bind(this), false);
};
this.view = view;
return this;
}
return new ViewController(view);
}
// implement
var keyboard = addViewController(window);
keyboard.listen();
- Note:
.bind()
is compatible with ECMAScript 5+; if you need a solution for older browsers, Mozilla has posted a great alternative to.bind()
usingfunctions
and.call()
:
https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Function/bind
Edit: Here's what your instantiated keyboard
object will look like using this new, modular solution:
Solution 2
Keyboard.prototype.listen = function() {
var self = this;
window.addEventListener('keydown', function(event) {
self.handle_keydown(event);
// self is your Keyboard object. You can refer to all your properties from this
});
}
How this code works:
- We are creating variable self, which stores reference to
this
variable. - The inner function is a closure, hence it has reference to self.
- When the closure function is called:
this
points to the dom object, whileself
points to keyboard object. - The closure is called with
event
as a parameter that we pass on to the member function of the keyboard object.
Solution 3
How about
function Keyboard() {
this.keys = {};
var self = this;
this.handle_keydown = function(args) {
self.keys[args.keyCode] = true;
}
this.listen = function() {
window.addEventListener('keydown', this.handle_keydown);
}
}
app.util.keyboard = new Keyboard();
Related videos on Youtube
Comments
-
Raf almost 2 years
I am a C# developer experimenting with JavaScript and I'm trying to get my head around the scope :)
I have the following code which contains an
addEventListener
in which I want to use a field from my object:(function(window) { function Keyboard() { this.keys = {}; } Keyboard.prototype.handle_keydown = function(args) { this.keys[args.keyCode] = true; } Keyboard.prototype.listen = function() { window.addEventListener('keydown', this.handle_keydown); } app.util.keyboard = new Keyboard(); })(window);
I would like to use the keys array in my hander, but understand that I cannot access is by using this, because this is the window in that context (correct?). If I change it to
app.util.keyboard.keys[args.keyCode] = true;
it works, but I'm not sure that's a good way to fix it.
I found this question, which seems rather similar, but Im not sure how I can fit it into my example.
Thanks for your help!
-
Musa over 11 yearsDo you have to add the functions through prototype, can you place them in the Keyboard function instead?
-
Foreign Object over 11 yearsIs there a reason why you're using a self-executing function?
-
Raf over 11 years@Musa I would like to call the listen function in a different function than where I create the Keyboard, so I think I have to?
-
Raf over 11 years@ClaytonMisura I think I'm doing it to also guarantee my variables scoped to the Keyboard 'class'?
-
Foreign Object over 11 yearsThat's true, and that seems like the responsible way to use it, just wondering.
-
-
Raf over 11 yearsno, I dont think so, could you show me the complete code maybe?
-
Raf over 11 yearsThat means I'm not using the prototype way, but I want only one keyboard instance, so maybe thats not a problem? Prototype is also something very new for me :)
-
closure over 11 years@Raf: This is complete code. I am calling your object with the correct handle. The event handler, which is part of your object getting called. Where is the issue? We don't need any other change in your code.
-
Raf over 11 yearsWoops, sorry, now it works! Don't know what I did before, but its ok now.
-
Raf over 11 yearsWas not my downvote ;) So do I understand correctly that this creates a self variable referencing my keyboard that is scoped to the window? If so, would this cause problems if I use this 'self' pattern in another 'class' (causing self to be overwritten and not being my keyboard anymore)?
-
closure over 11 years@Raf: Edited the answer with explanation. Thank you for accepting the answer.
-
Raf over 11 yearsThank you for the explanation, I think I understand it now! So the problem with self being overwritten cannot happen, because self is created within the closure of the function and only known in that closure then, right?
-
closure over 11 yearsself is created in the containing function (listen). The closure function in inside 'listen', hence it has access to all the variables in 'listen'
-
Raf over 11 yearsThe world is still here and I have learned some things about closure... What a great day! :)
-
nkint about 9 yearsvery clear answer but how to do if I need to use also
removeEventListener
? I need a named function.. ho to use thebind
thing? -
Benny Schmidt about 9 years@nkint Then just name that function and use it as the argument instead of the anonymous function used above. For example
addEventListener('keydown', handler)
instead ofaddEventListener('keydown', function () {})
. This way, you can later remove it by reference. -
Pavel Hasala over 6 yearsBut by doing this, you lose "this", because the scope (this) is changed to element that called event.
-
Pavel Hasala over 6 years@nkint addEventListener("click", this.onclick.bind(this), false)