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!
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
) on Keyboard.prototype
, but the keydown
event is actually fired on window
-- 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 to window
unless you bind it to another object via bind
or apply
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();
.bind()
is compatible with ECMAScript 5+; if you need a solution for older browsers, Mozilla has posted a great alternative to .bind()
using functions
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: