It took a few years, but I finally understand why member functions of classes in JavaScript aren't automatically bound to their objects, in particular when used in callback arguments.
In most object-oriented languages, functions are members of a class--they exist in memory only once, and when they are called, this
is
simply a hidden argument of the function which is assigned to the object that's calling it. Python makes this explicit by requiring that
the first argument of a class's member function be self
(Python's equivalent of this
--you can name self
whatever you want, but
self
is the convention).
class MyClass:
def f(self, someArg):
return self
In JavaScript, functions are "function objects". Any object or variable can reference the function because it's just a pointer, like any
other object. Unless the JavaScript engine has a reference to the object that references the function, it has no way of knowing what
this
should be assigned to.
Consider the following, perfectly legal script.
let f = function () { return this; },
o1 = { name: 'o1', f },
o2 = { name: 'o2', f };
o1.f().name; // returns 'o1'
o2.f().name; // returns 'o2'
f(); // returns window, global, or undefined depending on the environment and closure
The only way f
knows what this
is, is if there is an object it's being called from.
Classes in JavaScript are just syntactic sugar atop functions that have a prototype
property and that implicitly return an object.
When new
is called on the function, it calls the function and attaches everything in the function's prototype to the returned object.
class O {
constructor(name) {
this.name = name;
}
f() {
return this;
}
}
is exactly equivalent to
function O(name) {
this.name = name;
}
O.prototype.f = function () {
return this;
}
In both cases,
let o1 = new O('o1'),
o2 = new O('o2');
o1.f === o2.f // true
Now, suppose we have some callback.
function cb(callback) {
ㅤ return callback();
}
cb(o1.f); // returns undefined
In this case, we're passing only the reference to the function f
into cb()
without any attachment to o1
. Since it's just a
reference to a function object, the engine has no way of knowing that callback
is a member of o1
, so this
cannot be assigned a
value. After all, f
is also a member of o2
.
Because this is a frustrating artifact of JavaScript's function objects, there are three ways it addresses the issue. First, we can pass
an anonymous function into cb
that returns o1.f()
.
cb(() => o1.f()); // returns o1
Alternatively, if we know that we want to call f
of whatever object is passed, we can change the signature of cb
to
function cbCallF(o) {
ㅤreturn o.f();
}
cbCallF(o1); // returns o1
Second, we can bind()
a function to an object.
let bound = f.bind(o1);
cb(bound); // returns o1
f.bind(o1)
returns a "BoundFunction" object where this
within the context of the bound function is automatically (and always1) o1
, no matter
which function is calling f()
.
A common, if verbose, practice, then, is to bind member functions within the constructor of a class.
class O {
constructor(name) {
this.f = this.f.bind(this);
this.name = name;
}
f() {
return this;
}
}
Unfortunately, this approach requires a line calling bind
for every member that might be called in a callback. There are other, hacky
methods such as looping through all member functions via Object.getOwnPropertyNames()
and calling bind()
, but this can lead to its own
set of idiosyncratic problems.
Note that bind()
doesn't change f
; it merely returns a pointer to a function that calls o1.f()
. This is functionally equivalent to
(though internally different from) our anonymous function in the first approach.
Finally, with public field declarations--as of December 2021, available in all modern browsers (RIP IE)--we can declare a field as an arrow function.
class O {
constructor(name) {
this.name = name;
}
f = () => this;
}
As I pointed out earlier, class
es are simply syntactic sugar around function
s. The above is exactly equivalent to
function O(name) {
this.name = name;
this.f = () => this;
}
Since f
is a field/property, it is not attached to the O
s prototype.
O.prototype.f // => undefined
In all three cases, there is a function attached to the individual instances of O
, which takes up O(n) memory for n instances of class O
. This seems like a drawback of
JavaScript versus other object-oriented languages, but, in fact, other languages simply can't pass functions as objects as easily as
JavaScript can, and when they do pass them (e.g. C#'s Func
class), they pass them as instances of objects, the same as JavaScript.
I wanted to see if I could make a succinct auto-binding mechanism. These already exist (e.g. auto-bind) but just as a personal challenge I wanted to write my own. (It also turns out that auto-bind doesn't handle every case2.)
Function.prototype.bindMethods = function (o, ...excludeMembers) {
let p = this.prototype,
exclude = new Set(excludeMembers);
Object.getOwnPropertyNames(p)
.concat(Object.getOwnPropertySymbols(p)) // this is where the aforementioned auto-bind library falls short
.filter(n => p[n] instanceof Function && p[n] !== p.constructor && !exclude.has(n))
.forEach(n => o[n] = o[n].bind(o));
}
or
Object.prototype.bindMethods = function (...excludeMembers) {
let p = this.constructor.prototype,
exclude = new Set(excludeMembers);
Object.getOwnPropertyNames(p)
.concat(Object.getOwnPropertySymbols(p))
.filter(n => p[n] instanceof Function && p[n] !== p.constructor && !exclude.has(n))
.forEach(n => this[n] = this[n].bind(o));
}
Test cases
const s = Symbol('o1');
class O {
constructor(name) {
this.name = name;
O.bindMethods(this);
// or
// this.bindMethods();
}
f() {
return this;
}
[s]() {
return this.name;
}
}
let o1 = new O('o1'),
o2 = new O('o2');
function cb(callback) { return callback(); }
cb(o1.f); // returns o1;
cb(o2.f); // returns o2;
cb(o1[s]); // returns 'o1'
cb(o2[s]); // returns 'o2'
Alternatively, if you don't want to pollute Function.prototype
or Object.prototype
, you can use this function:
function bindMethods(o, ...excludeMembers) {
let p = o.constructor.prototype,
exclude = new Set(excludeMembers);
Object.getOwnPropertyNames(p)
.concat(Object.getOwnPropertySymbols(p))
.filter(n => p[n] instanceof Function && p[n] !== p.constructor && !exclude.has(n))
.forEach(n => o[n] = o[n].bind(o));
}
class O {
constructor(name) {
// ...
bindMethods(this);
}
// ...
}
Footnotes
-
A bound function cannot be re-bound to a different
this
object. Callingbind()
on a bound function returns a new bound function with the original binding.const b1 = f.bind(o1); b1().name // 'o1' const b2 = b1.bind(o2); b2().name // 'o1' b1 === b2 // false
This is because
bind()
has a second purpose: binding values to a function's arguments.
↩function foo(a, b, c) { console.log(a, b, c); } const bound1 = foo.bind(null, 'a'); bound1('b', 'c'); // prints a b c const bound2 = bound1.bind(null, 'b'); bound2('c'); // prints a b c
-
If you are using a compiler such as babel or TypeScript, you can enable decorators (babel/plugin-proposal-decorators,
experimentalDecorators
intsconfig.json
), and then you can use autobind-decorator which is the best implementation I've seen--better than mine.Not shown in the README, you can still use this without decorators, though it takes the magic out of it.
const { boundClass, boundMethod } = require('autobind-decorator'); const MyAutoboundClass = boundClass(class MyClass { /* ... */ }); class MyClass { myAutoboundMethod() { /* ... */ } } MyClass.prototype.myAutoboundMethod = boundMethod(MyClass.prototype.myAutoboundMethod);
This is what desugared decorators do, though the latest proposal (as of 12/2021) includes metadata about the value being decorated. (The latest versions of compilers may also include this--I don't know.) ↩
Know this is old, but I was having trouble understanding why methods weren't bound to instances after coming from python, and this helped a lot. Thank you!