Tuesday, April 1, 2014

TypeScript: "this" is what I want! (the unfortunate neglect of Instance Methods / callback functions)

I was recently reading Jeff Walker's blog post "Why TypeScript Isn't the Answer". This is part of series in which Jeff goes through various compile-to-JavaScript technologies including TypeScript, CoffeeScript and Dart and explains his view of why he feels they don't quite hit the mark.

As a user (and big fan) of TypeScript I read the post with interest and picked up on one particular issue that Jeff mentions:

Classes make the unchanged behaviour of the this keyword more confusing. For example, in a class like Greeter from the TypeScript playground, the use of this is confusing:

class Greeter {
 greeting: string;
 constructor(message: string) {
  this.greeting = message;
 }
 greet() {
  return "Hello, " + this.greeting;
 }
}

One can’t help but feel the this keyword in the methods of Greeter should always reference a Greeter instance. However, the semantics of this are unchanged from JavaScript:

var greeter = new Greeter("world");
var unbound = greeter.greet;
alert(unbound());

The above code displays “Hello, undefined” instead of the naively expected “Hello, world”.

Now Jeff is quite correct in everything he says above. However, he's also missing a trick. Or rather, he's missing out on a very useful feature of TypeScript.

Instance Methods to the Rescue!

Still in the early days of TypeScript, the issue Jeff raises had already been identified. (And for what it's worth, this issue wasn't there by mistake - remember TypeScript is quite deliberately a "superset of JavaScript".) Happily with the release of TypeScript 0.9.1 a nice remedy was included in the language in the form of "Instance Methods".

Instance Methods are lexically scoped; bound to a specific instance of a JavaScript object. i.e. These methods are *not* vulnerable to the “Hello, undefined” issue Jeff raises. To quote the blog post:

We've relaxed the restrictions on field initializers to now allow 'this'. This means that classes can now contain both methods on the prototype, and callback functions on the instance. The latter are particularly useful when you want to use a member on the class as a callback function, as in the code above. This lets you mix-n-match between ‘closure’ style and ‘prototype’ style class member patterns easily.

Greeter with Instance Methods

So, if we take the Greeter example, how do we apply Instance Methods to it? Well, like this:

class Greeter {
 greeting: string;
 constructor(message: string) {
  this.greeting = message;
 }
 greet = () => {
  return "Hello, " + this.greeting;
 }
}

Can you tell the difference? It's subtle. That's right; the mere swapping out of () with = () => on the greet method takes us from a prototype method to an Instance Method.

Observant readers will have noticed that we are using TypeScript / ES6's Arrow Function syntax. In fact with that in mind I could actually have gone super-terse if I was so inclined:

class Greeter {
 greeting: string;
 constructor(message: string) {
  this.greeting = message;
 }
 greet = () => "Hello, " + this.greeting
}

But either way, both of the above class declarations compile down to the following JavaScript:

var Greeter = (function () {
    function Greeter(message) {
        var _this = this;
        this.greet = function () {
            return "Hello, " + _this.greeting;
        };
        this.greeting = message;
    }
    return Greeter;
})();

Which differs from the pre-Instance Methods generated JavaScript:

var Greeter = (function () {
    function Greeter(message) {
        this.greeting = message;
    }
    Greeter.prototype.greet = function () {
        return "Hello, " + this.greeting;
    };
    return Greeter;
})();

As you can see the Instance Methods approach does *not* make use of the prototype on Greeter to add the method. (As the pre-Instance Methods greet() declaration did.) Instead it creates a function directly on the created object and internally uses the _this variable inside the Instance Methods. (_this being a previously captured instance of this.)

So with Instance Methods we can repeat Jeff's experiment from earlier:

var greeter = new Greeter("world");
var bound = greeter.greet;
alert(bound());

But this time round the code displays “Hello, world” and no longer “Hello, undefined”.

Update 02/04/2014 - mixing and matching prototype and Instance Methods

Bart Verkoeijen made an excellent comment concerning the extra memory that Instance Methods require as opposed to prototype methods. Not everyone reads the comments and so I thought I'd add a little suffix to my post.

What I’ve come to realise is that it comes down to problem that you’re trying to solve. Instance methods are bulletproof in terms of relying on a specific instance of this regardless of how a method is invoked. But for many of my use cases that’s overkill. Let’s take the original (prototype methods) Greeter example:

var Greeter = (function () {
    function Greeter(message) {
        this.greeting = message;
    }
    Greeter.prototype.greet = function () {
        return "Hello, " + this.greeting;
    };
    return Greeter;
})();

var greeter = new Greeter("world");
var greeter2 = new Greeter("universe");

console.log(greeter.greet()); // Logs "Hello, world"
console.log(greeter2.greet()); // Logs "Hello, universe"

As you can see above, provided I invoke my greet method in the context of my created object then I can rely on this being what I would hope.

That being the case my general practice has not been to use exclusively Instance methods *or* prototype methods. What I tend to do is start out only with prototype methods on my classes and switch them over to be an Instance method if there is an actual need to ensure context. So my TypeScript classes tend to be a combination of prototype methods and Instance methods.

More often than not the prototype methods are just fine. It tends to be where an object is interacting with some kind of presentation framework (Knockout / Angular etc) or being invoked as part of a callback (eg AJAX scenarios) where I need Instance methods.

2 comments :

  1. Thanks for pointing this out. I've been using the technique of "instance methods" already in my current JavaScript code, and happy that it is easy to achieve with TypeScript. It's nothing more than keeping a reference to this in a closure, frequently seen as `var that = this;`.

    However, I've always wondered what would be the impact on performance and memory consumption with this approach. Since functions are objects, and using this technique could potentially create many objects per instance. Prototypes should help to only have single function definition objects in memory. Are JavaScript engines smart enough to realize that a certain instance method may already have been defined in memory, and that it can reference to the existing function object, instead of creating a new one?

    Any thoughts on this, or perhaps an answer?

    ReplyDelete
    Replies
    1. That’s an excellent question!

      I've a couple of thoughts. Initially when I heard of function.bind I thought that solved this particular problem. However when I looked further I discovered that the bind method creates a new function rather than ensuring that ‘this’ has a specific value when methods are invoked. (Read more here: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/bind ) So bind has the same memory issues that Instance methods have.

      What I’ve come to realise is that it comes down to problem that you’re trying to solve. Instance methods are bulletproof in terms of relying on a specific instance of ‘this’ regardless of how a method is invoked. But for many of my use cases that’s overkill. Let’s take the original (prototype methods) ‘Greeter’ example:

      var Greeter = (function () {
      function Greeter(message) {
      this.greeting = message;
      }
      Greeter.prototype.greet = function () {
      return "Hello, " + this.greeting;
      };
      return Greeter;
      })();

      var greeter = new Greeter("world");
      var greeter2 = new Greeter("universe");

      console.log(greeter.greet()); // Hello, world
      console.log(greeter2.greet()); // Hello, universe

      As you can see above, provided I invoke my ‘greet’ method in the context of my created object then I can rely on ‘this’ being what I would hope.

      That being the case my general practice has not been to use exclusively Instance methods *or* prototype methods. What I tend to do is start out only with prototype methods on my classes and switch them over to be an Instance method if there is an actual need to ensure context. So my TypeScript classes tend to be a combination of prototype methods and Instance methods.

      More often than not the prototype methods are just fine. It tends to be where an object is interacting with some kind of presentation framework (Knockout / Angular etc) or being invoked as part of a callback (eg AJAX scenarios) where I need Instance methods.

      In all honesty it’s been quite a while since I encountered a situation where performance was an issue and so I wouldn’t be over-worried about using Instance methods only. That said, I always try and design for performance and so I’m unlikely to ever do that.

      Thanks for the question – good thing to think about.

      Delete