Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Why JavaScript class members are not automatically bound to the instance of the class

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 = { val: 1, f },
    o2 = { val: 2, f };

o1.f().val;   // returns 1
o2.f().val;   // returns 2
f();          // returns window or global depending on whether it's running in a browser or not

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(v) { 
    this.val = v;
  }

  f() {
    return this; 
  }
}

is exactly equivalent to

function O(v) {
    this.val = v;
}

O.prototype.f = function () {
    return this;
}

In both cases,

let o1 = new O(1),
    o2 = new O(2);
    
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 two 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 cb(o, callback) {
ㅤreturn callback(o.f());
}

cb(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 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(v) {
        this.f = this.f.bind(this);
        this.val = v;
    }

    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.

In both cases, there is a wrapper function, 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 case.)

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(1);
    
class O {
    constructor(v) {
        this.v = v;
        
        O.bindMethods(this);
        // or
        // this.bindMethods();
    }
    
    f() {
        return this;
    }
    
    [s]() {
        return this.v;
    }
}

let o1 = new O(1),
    o2 = new O(2);
function cb(callback) { return callback(); }
cb(o1.f);    // returns o1;
cb(o2.f);    // returns o2;
cb(o1[s]);   // returns 1
cb(o2[s]);   // returns 2

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(v) {
        // ...
        bindMethods(this);
    }
    // ...
}
@lambdabyte

This comment has been minimized.

Copy link

@lambdabyte lambdabyte commented Sep 27, 2020

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!

@CoinBR

This comment has been minimized.

Copy link

@CoinBR CoinBR commented Jan 26, 2021

Very helpful, and well explained!
I think I finally wrapped my head around it.

@aksperiod

This comment has been minimized.

Copy link

@aksperiod aksperiod commented Mar 24, 2021

thank you for this!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment