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.