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:
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:
this
variable. this
points to the dom object, while self
points to keyboard object.event
as a parameter that we pass on to the member function of the keyboard object.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With