ponyfoo.com

Binding Methods to Class Instance Objects

Fix
A relevant ad will be displayed here soon. These ads help pay for my hosting.
Please consider disabling your ad blocker on Pony Foo. These ads help pay for my hosting.
You can support Pony Foo directly through Patreon or via PayPal.

There’s a number of different ways we can ensure class methods have access to this. After some controversy on Twitter last week, we quickly go over the most common approaches, discussing their advantages and drawbacks.

The problem is when we have a class method such as the following.

class Logger {
  printName (name = 'there') {
    this.print(`Hello ${name}`);
  }

  print (text) {
    console.log(text); 
  }
}

Then – for whatever reason – the printName method’s context changes, expectations aren’t met, exceptions are thrown, and kittens die.

const logger = new Logger();
const { printName } = logger;
printName();
// <- Uncaught TypeError: Cannot read property 'print' of undefined

Twitter drama began with André’s salty remarks about a competing front-end framework.

The problem lies, of course, with the fact that JavaScript doesn’t yet have semantics where we can easily mark every class method as bound to that class, which is how they probably should work.

Let’s look at our options.

Caveman Mode

In this scenario, we just bind methods by hand on the constructor.

class Logger {
  constructor () {
    this.printName = this.printName.bind(this);
  }

  printName (name = 'there') {
    this.print(`Hello ${name}`);
  }

  print (text) {
    console.log(text); 
  }
}

This is the least elegant solution, but it works. Drawbacks include having to keep track of which methods use this and need to be bound, or ensuring every method is bound, remembering to .bind new methods as they are added, and removing .bind statements for methods that are removed. Benefits include being explicit, and having no extra code involved.

Auto Bind

A similar but less painful approach is using a module that takes care of this on our behalf. Sindre’s auto-bind goes through an object’s methods and binds them to itself.

class Logger {
  constructor () {
    autoBind(this);
  }

  printName (name = 'there') {
    this.print(`Hello ${name}`);
  }

  print (text) {
    console.log(text); 
  }
}

This approach works well for classes, although we can’t escape the need for a constructor. The advantage is that we don’t have to keep track of every single method by name when binding them. At the same time, if we’re dealing with objects rather than classes, we need to ensure that autoBind gets called on the object after every method has been assigned to the object, or else some methods will be left unbound. Any methods added after autoBind gets called are unbound, and this means that in some situations autoBind is an even worse option than manually calling .bind on every method.

Proxies

A Proxy object could be used to intercept get operations, returning methods bound to the logger. Below we have a selfish function which takes an object and returns a proxy for that object. Any methods accessed through the proxy will be automatically bound to the object. A WeakMap is used to ensure we only bind the methods once, so that equality in proxy.fn === proxy.fn is preserved.

function selfish (target) {
  const cache = new WeakMap();
  const handler = {
    get (target, key) {
      const value = Reflect.get(target, key);
      if (typeof value !== 'function') {
        return value;
      }
      if (!cache.has(value)) {
        cache.set(value, value.bind(target));
      }
      return cache.get(value);
    }
  };
  const proxy = new Proxy(target, handler);
  return proxy;
}
const logger = selfish(new Logger());

The benefit of taking this approach is that methods on the target object will be bound regardless of whether they were added before or after the proxy object was created. The obvious drawback is that Proxy support is meager even when transpilers are taken into account.

Even if proxies were generally available, this solution would also be far from ideal. The difference lies in that libraries could potentially implement something like selfish so that components you hand over to the library would follow these semantics without you having to do anything. That said, the only real solution lies in the language moving forward, adding semantics for classes that have every method bound to itself by default.

We’re definitely better off than back when we had to write Logger.prototype.print – an implementation detail artifact that never made a lot of sense – and classes are a result of observing a pattern that was being repeated consistently, over time, and developing a solution. A private scope for classes where you can declare functions scoped to that class that aren’t class methods (but are available to every method) and other variables scoped to the class would be a huge step forward for classes.

Having bind semantics that are a bit less verbose, tedious, error-prone, or complicated, is mostly a matter of time. And when it comes to JavaScript, we all know that time flows faster. ⏳

What’s your preferred way of ensuring methods are bound to their host object? What native JavaScript semantics would you propose for objects where every method is bound to their host?

Liked the article? Subscribe below to get an email when new articles come out! Also, follow @ponyfoo on Twitter and @ponyfoo on Facebook.
One-click unsubscribe, anytime. Learn more.

Comments