Can we fix "this"?

No.

But, perhaps there are ways to reduce the foot-gunny-ness of "this".

I gave roughly the following question to nine different JavaScript developers: Will the following code snippet work?

class User {
  constructor(name) {
    this.name = name
  }
 
  printName() {
    console.log(this.name)
  }
}
 
const sarah = new User('Sarah')
setTimeout(sarah.printName, 1000)

Six of the nine got this wrong, and I don't blame them. "this" is a little confusing. People just expect that you can pluck methods off of an instance and run it in isolation. Eventually, it would be good for them all to learn how "this" working in javascript, but in the meantime, perhaps there's stuff we can do to make "this" match people's expectations more often.

Proposal: Automatic this binding

On the surface, the proposal works like this: When you declare a class, prefix the declaration with "bound", like this:

bound class User {
  ...
}

Whenever you access a method from an instance of an auto-binding class, the correct "this" value will automatically be bound to the method. Thus, you can write code like this, and it'll work as expected:

const sarah = new User('Sarah')
setTimeout(sarah.printName, 1000)

How does this black magic work?

Normally we place methods on the prototype. In an auto-binding class, we'll be placing getters instead that return methods that have been bound in a special way. It works like this:

The code "sara.printName" will invoke the "printName" getter.
The getter will create a new function that's exactly the same as the method definition found within the class.
printName will capture its "this" value, which in the above example is the sarah object, and place it into an internal slot on this new function. Lets call the slot [[strictBind]].
The "strictly bound" function is returned.

When the function gets invoked, the following happens:

The [[strictBind]] slot will be compared against the [[homeObject]] slot. If [[strictBind]] does not inherit from [[homeObject]] an error will be thrown. What this means in practice is that an error would be thrown if you tried to do User.prototype.printName(), as both the [[homeObject]] slot and the [[strictBind]] slot would be equal to User.prototype. (For those who don't know, [[homeObject]] is automatically defined on methods to be the class's prototype. In my above example, User.prototype.printName.[[homeObject]] === User.prototype)
The function is then invoked as normal, using the [[strictBind]]'s value as the implicit this parameter.

Some additional notes:

  • While I explained the getter logic in terms of creating new functions each time it gets accessed, this doesn't mean we have to end up with odd scenarios like user.printName !== user.printName. Equality and what-not can be defined as you would expect.
  • The getters on the prototype don't have to be literal getters - e.g. when you do object.getownpropertydescriptor on an instance's prototype, it can still show that those fields are normal fields. The engine will be applying special logic whenever you access the value from these fields, but for everyone else, these fields can look like normal ones. It does make the prototype object a bit exotic, but that's ok. The exact behavior of this can certainly be a discussion point.
1 Like

Automatic this-binding is how React.createClass works, and as I recall, one of the major issues with it was that performance suffered a lot due to the largely-unnecessary binding of every method. Admittedly, it wasn't using a getter to return a bound function (which, incidentally, would either have to memoize and create a memory leak, or, create a brand new function every time).

I don't think this is worth fixing, personally - if you're writing OO code like that, you're pretty unlikely to be passing around methods at all, let alone as first-class callbacks.

I don't think this is worth fixing, personally - if you're writing OO code like that, you're pretty unlikely to be passing around methods at all, let alone as first-class callbacks.

There aren't many JavaScript programmers who go full OOP, just like there aren't many who go full FP. There tends to be a lot of mixing and matching.

When I worked in React, I recall having to commonly avoid this exact pitfall with their class-based components, by explicitly binding callbacks.

class MyElement extends React.Element {
  constructor() {
    setTimeout(this.onClickHandler.bind(this), 1000) // Don't forget to use .bind() here
  }

  render() {
    return (
      // Don't forget to use .bind() here
      <button onClick={this.onClickHandler.bind(this)}>
        Click me!
      </button>
    )
  }

  onClickHandler(event) { ... }

  timeoutHandler() { ... }
}

Of course, many people in the React community have moved on from class-based components and now use function components, but I think the problem is still prevalent. Many JavaScript APIs require callbacks, JavaScript thrives off of them, and many people are going to want to put their callback handlers into the class.


Automatic this-binding is how React.createClass works, and as I recall, one of the major issues with it was that performance suffered a lot due to the largely-unnecessary binding of every method. Admittedly, it wasn't using a getter to return a bound function (which, incidentally, would either have to memoize and create a memory leak, or, create a brand new function every time).

I don't really know the performance implications of what I'm proposing. I hope there's ways around it, but I have no idea.

If an engine were to go with a memoization route, I don't think that necessarily means that there would be a memory leak. They could store the "this" instances as keys in a WeakMap, and the function instances as values.

Oh! Regarding performance, I just realized there's an easy solution to it.

As I've described the process above, every time you do instance.method(), a new method is being created that's "strictly" bound to instance. Most of the time when people are doing instance.method, it's because they immediately want to call the method. An engine can simply skip the whole "make a new special method" step and directly call the original method implementation using instance as the "this" value. The only time auto-binding really needs to happen is when you do instance.method without calling it.

In this design developers now have even more this behaviors to learn. Sometimes it’s safe to pass a direct reference to a class method, sometimes it’s not.

Personally I think it’s better to err on the side of caution, and only pass direct function references instead of binding them or wrapping in an arrow when confident that the receiver is already bound or unimportant.

You can't fix "this". To me the way it works in JS doesn't make sense, but it is what it is. Adding another behaviour would just increase confusion.

I've seen this more than once this week. It's err on the side of caution. — Grammar Nazi

1 Like

Thanks!

I think the standard response to this is going to be

image

There's also the bind operator which can make things a little easier, but again, something else to learn and have to know about

setTimeout(::sarah.printName, 1000)

The problem with this is something you point out here:

  • While I explained the getter logic in terms of creating new functions each time it gets accessed, this doesn't mean we have to end up with odd scenarios like user.printName !== user.printName. Equality and what-not can be defined as you would expect.

Without this equality you run into problems where, for example with DOM event listeners, to remove a listener you'd need a reference to the same function used to add it.

document.addEventListener('click', ::sarah.printName)
// ...
document.removeEventListener('click', ::sarah.printName) // Fails, different function

I'm pretty sure someone once proposed a version of bind which maintains equality here, but after a quick search I wasn't able to dig that up.

@senocular

Oh, that's an interesting issue with the bind operator that I haven't thought of. And, that would presumably be a common use for it.

I think decorators would bring us halfway there, but there would be important limitations in what a decorator could do. For example:

  • The equality thing as you pointed out.
  • Performance - a new function would have to be created (or received from a memoized cache) every time you do instance.method() in a decorator approach.

@aclaymore @lightmare

So a big worry seems to be about overloading "this" with too many behaviors. What if we went a different direction. I was debating between two different angles to approach this problem, the other solution would be to introduce a new "#self" keyword altogether. It works like this.

You don't declare an entire class as "auto-binding" anymore, instead, any method that uses "#self" is auto-self-binding, and will be present on the class's prototype as a getter.

#self can only be used inside of class syntax. It will always refer to the instance. "this" behavior will not be changed.

This means the following:

class MyClass {
  x = 2
  f() {
    console.log(this.x)
    console.log(#self.x)
  }
}

const f = new MyClass().f
f.call({ x: 3 }) // this.x logs 3 while #self.x logs 2.

A #self keyword would have other interesting benefits, for example, it can be used correctly within a nested function.

class MyClass {
  x = 2
  f() {
    return function () {
      return #self.#x // #self.x still refers to the "x" property on the instance.
    }
  }
}

Basically, "#self" would follow all of the expectations people have with "this". Always. The only time people need to be careful is when they're using a legacy method that utilizes "this" instead of "#self", at which point, they have to know all of the details about how "this" works to properly know when to bind it and when not to.


Regarding this point @aclaymore

I think this should continue to hold true after a proposal like this as well. If you're using a third-party library or the standard library and want to pick off a method, you probably shouldn't assume they're using auto-binding protection. It's best to bind it anyways (unless, you've seen it documented that they use auto-binding).

The biggest benefit such a proposal would help out with is for when you're using your own code. I wouldn't be surprised if one of the biggest source of bind hazards comes from when the instance you're using is "this". Take my React code as an example, all of the this.method.bind(this)s could go away within that class definition if the class was declared as "bound", or if it used #self. A developer could also know when their whole codebase is recent enough to always use "bound" or "#self", then they can freely pick methods off of instances of classes that were defined within their codebase.

Wouldn't the magic #self face the same issues?
o.f has to return something that binds #self = o, and has to be memoized to have o.f === o.f

Separate from that conflicting with a private field of that name, what about:

f() {
addCallback(() => #self);
}

?

The intuitive thing would be that it has the same rules as this - we can’t make this simpler by adding another form of it.

I think an explicit `@bounds decorator would be much simpler a solution.

1 Like

@lightmare

If you do o.f(), the engine can optimize away the fact that a special function is being created and returned when you access f. Instead, the engine can just call the original definition of f, passing in o as #self. So, performance isn't an issue here either. The only time a special function really needs to be created is when you do const callback = o.f.

And, memoization doesn't need to happen to make o.f === o.f. Think of the record/tuple proposal. They define two things to be equal when their contents are equal. i.e. #[2] === #[2]. Similarly, two of these special methods will be considered equal if their "contents" are equal - contents being 1. if they were created from the same definition, and 2. if their self-bound instances are equal.


@ljharb

Well, in that specific example, the "#self" would refer to the same thing as "this" assuming f() was called in a normal way, because you used an arrow function :). The differences with callbacks would only be seen if you used the long-form syntax of declaring a function. Edit: If we found that unintuitive, we could always ban "#self" from nested callbacks, but I personally find it to be intuitive to understand that "#self" always refers to the instance of the class. Period.

But, perhaps having a built-in @bounds decorator could be a viable path forwards. I'm realizing that, perhaps, built-in decorators don't need to be bound by the same limitations as user-defined ones. A built-in decorator could still utilize slots and what-not, which means an engine could still provide special optimizations.

A userland one can too, by using a closed-over WeakMap.

I wouldn’t put too much hope in engine optimizations - a feature that requires an optimization to be viable is unlikely to ever be accepted.

Is this particular optimization that much different from the one you were hoping for in the bind proposal (see your comment here)? One of the things you really wanted in bind syntax was for the ability for an engine to tell the difference between when you're just wanting to extract a method, vs when you're wanting to call a method.

That means the engine can tell the difference between these two forms, and optimize them independently.

instance::myFunction()
instance::myFunction

The optimization being asked for here isn't too much different. The engine needs to tell the difference between these two forms and optimize them appropriately.

instance.myFunction()
instance.myFunction

This is something that can also be written into the spec and required.

A userland one can too, by using a closed-over WeakMap.

Sure, but that's a whole lot of magic for userland code. I wouldn't recommend anyone do this unless it was provided by the language itself, or maybe a framework (frameworks tend to be able to get away with more stuff).

Thanks, that's an important piece I was missing. Still, I'd favour bind operator, which would be immediately useful without touching existing code.

In that case, I’d probably want the spec to actually have two forms, rather than leaving it up to a possible optimization. In this case, there’s no special syntax at the callsite, so that isn’t an option.

As for this part, no, this is not an option - if you mutate one of them and the property doesn’t show up on the other one, then they can’t be === - that’s an axiom of the language for objects.

Records and Tuples only can get away with this because they’re primitives. Functions are objects,

@lightmare

Well, I don't think the bind operator and this proposal are necessarily mutually exclusive (sort of - there's certainly a number of bind operator use cases that this proposal would handle, but there are also a number of others that it wouldn't handle).

And, I believe there will be immediate uses for a built-in @bounds decorator. In an existing codebase, you can start using it on any new classes - whenever you want to pick a method off, instead of using this.method.bind(this), you can just do this.method. It also wouldn't be a big refactor to, every once in a while, change an existing internal class to use the @bounds decorator and removing all occurances of this.method.bind(this). Such a refactor isn't much bigger than changing existing .binds() to use a bind syntax. I would venture to guess that most ".bind()"s happen using "this" - you would be able to improve on all of those particular use cases immediately, at your leisure. The one thing you shouldn't do in an existing code base is start trying to trust that instances you made from other classes would be safely auto-bound because they might not have been refactored that way yet.

@ljharb

Oh, hmm, good point.

Actually, I think it could still work. In explaining this feature, it's easier to do so by saying we're creating different functions, but my hope is that we can find a way to make it feel like it's always the same function to the end user.

I've been going back and forth on how best to do equality. Let's change it slightly from what I said before - for two special functions to be considered equal, they simply must be created from the same function definition. No comparing of bound this value. This means instance1.f === instance2.f.

When you get, set, or delete a property from one of these special functions, you'll actually be modifying a shared space. Perhaps it's better to think of these functions as proxy-like - they all know what their auto-bound this value is, and they know where the function definition is that they proxy their behaviors to. So this would work:

obj.f.x = 2
console.log(obj.f.x) // 2

because each time you access a property on obj.f, you'll be references a space shared by all f methods of that class.

All of these would be complexities that the spec handles. To the end-user, they just intuitively understand that for all intents and purposes, obj.f === obj.f.

I'm no expert with the spec, but I believe it might be possible to have the two forms. You would have to change the existing spec, so that obj.f and obj.f() result in two separate actions being performed. obj.f() syntax would now cause some new abstract operation to be used, say, [[getAndCall]]. For most objects, that operation would be defined to be the same as how [[get]] is currently defined followed by a [[call]]. We can then make all instances of an auto-binding class exotic in that it redefines [[get]], such that when a method is accessed, it auto-binds it's this value. When a [[getAndCall]] operation happens instead, everything will behave as normal.

Or, something like that. I don't really have any clue what I'm talking about - the spec is all pretty foreign to me. But I believe it should be possible.

That's a no-go. Now you can't reliably use those as (removable) callbacks.

Oh, right. Hmm...

All right, so I guess there's two other options.

  1. Freeze the methods. You can't modify them.

  2. Make it so instance1.f !== instance2.f and make the shared method space specific to each instance.

I think my preference would be with the first (freezing them). These methods are on the prototype, you would expect that instance1.f === instance2.f.