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 selfIn 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 closureThe 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 // trueNow, suppose we have some callback.
function cb(callback) {
ㅤ return callback();
}
cb(o1.f); // returns undefinedIn 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 o1Alternatively, 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 o1Second, we can bind() a function to an object.
let bound = f.bind(o1);
cb(bound); // returns o1f.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, classes are simply syntactic sugar around functions. 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 Os prototype.
O.prototype.f // => undefinedIn 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
thisobject. 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 // falseThis 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,
experimentalDecoratorsintsconfig.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!