Thursday 17 March 2016

JavaScript: clarifying in my head this and that and proxies

G'day:
This came up in our team meeting the other day, We write a reasonable wodge of JavaScript, and its of varying degrees of veneration, quality and uniformity (if that last one isn't an oxymoron). One of the quality-variations we have is how we handle our event handlers specifically in the context of how they access the parent-object's this reference. Now I haven't read-up thoroughly on how this works in JS, but I know enough to get by. Until I don't. I think many people are like this. Anyway, within an object, this refers to the object itself. Within an event handler, it refers to the object which caused the event to occur, and the handler to run. If the event handler method is in an object, and it needs to access both the object firing the event and the object the handler is in, one cannot use this to reference both of them.

That's be easier to visualise in code - and I will - but I'll finish some framing first. In our code base we use a mixture of a coupla different approaches:

  • use something like that = this to make a second reference to the object's this so the handler can reference this to refer to the object firing the event, and that to refer to its parent object. Bleah. That just reeks of "hack"
  • use JQuery's proxy() method to re-task the function's this to be its parent object's one, rather than the event-firing object.
Personally I prefer the latter version. It seems less hacky.

Anyhow, the meeting discussion got snarled up with the idea that if we use the proxy to re-purpose this, we then can't use this to reference the object firing the event, which we often have to do at the same time. This made us go "hmmm...", and then we ran out of time to come up with a satisfactory answer, and moved on.

I kept thinking about this and then kicked myself cos it was obvious. this in the context of the event handler is pretty much just syntactical sugar as far as I can tell, and it's all a bit magical. As well has having that event object exposed via this, it's also obviously exposed in the event argument that's passed to the handler. We don't need to use this to reference the event-firing object. Indeed IMO it's cleaner if we simply don't do that.

I had to get this clear in my head, so jotted some code down. Which is here:

<!doctype html>

<html lang="en">
<head>
    <meta charset="utf-8">
    <title>Event variables</title>
    <script src="https://code.jquery.com/jquery-2.2.1.min.js"></script>
</head>

<body>
    <button id="noProxy">Event handler without proxy</button>
    <button id="proxyRef">Using proxy reference</button>
    <button id="jqueryProxy">Event handler using jquery proxy</button>
    <button id="usingBind">Event handler using bind</button>

    <script src="./Page.js"></script>
    <script src="./init.js"></script>
</body>
</html>

So I've got some buttons which I can push.

And here's Page.js:

Page = function(page){
    this.page = page;
};

Page.bindHandlers = function(page){
    $("#noProxy").on("click", page.clickHandlerWithoutProxy);
    $("#proxyRef").on("click", page.clickHandlerWithProxyRef());
    $("#jqueryProxy").on("click", $.proxy(page.clickHandlerUsingJQueryProxy, page));
    $("#usingBind").on("click", page.clickHandlerUsingBind.bind(page));
};

Page.prototype.clickHandlerWithoutProxy = function(e){
    console.log("'e.target' refers to the button that triggered the event: " + $(e.target).text());
    console.log("'this' also refers to the button that triggered the event: " + $(this).text());
    console.log("Cannot reference 'this' from the Page object");
};

Page.prototype.clickHandlerWithProxyRef = function(){
    var self = this;
    return function(e){
        console.log("'this' refers to the button that triggered the event: " + $(this).text());
        console.log("'self' still refers to the Page object: " + self.page);
    }
};

Page.prototype.clickHandlerUsingJQueryProxy = function(e){
    console.log("'e' refers to the button that triggered the event: " + $(e.target).text());
    console.log("'this' still refers to the Page object: " + this.page);
};

Page.prototype.clickHandlerUsingBind = function(e){
    console.log("'e' refers to the button that triggered the event: " + $(e.target).text());
    console.log("'this' still refers to the Page object: " + this.page);
};


And init.js:

$(document).ready(function(){
    page = new Page(&quot;Home page&quot;);
    
    Page.bindHandlers(page);
});

Page defines a sorta class (as much as JS can), and within that there's a method to bind some event handlers to those buttons, and then the handlers themselves. Each bind and handler uses a different technique to access the event-firing object or the page object.

In clickHandlerWithoutProxy() we don't try to handle anything special... it just demonstrates that e.target refers to the object that the event was fired by, and also that this points to the same thing.

On the other hand clickHandlerWithProxyRef() exposes both the Page object's this (via a proxy variable self (we variously use that or self or probably _this too), and then the event handler's this still points to the event-firing object. Note how here I'm binding a method call to the event handler, and that method returns the actual handler, having first created that proxy self variable. This is an example of closure at work.

JQuery rescues us in the third option - clickHandlerUsingJQueryProxy() - $.proxy() let's one specify a different context for the event handler's this. This is all very cool and easy to do. It's predicated on using JQuery though. Which we do. And to access the event-firing object, we use the event argument passed into the handler, which has a target property which is that object. So we are accessing the handler's own object via this, and the event's object via target. Cool. And also doesn't have any surprises in the code: this always means the same thing, as is usage of e.target.

The last option - clickHandlerUsingBind - eschews JQuery and does the same thing using native JavaScript: the bind() method of the function object effects the same thing $.proxy() does. This was my research this morning, when I decided to find out how $.proxy() works. I think there is some browser compat issues with this... I read something about in IE8 needing to use a different approach. I still need to read up on that.

I'd err towards using the last approach, just using JS, but I think perhaps the JQuery approach is perhaps the safest here.

As I will say when I write about JavaScript, I am no expert in it at all. I know just enough to get by - this annoys me but it's the truth - so if I've got anything horribly wrong here, please do let me know!

Back to PHP...

--
Adam