Skip to content

Instantly share code, notes, and snippets.

@dfoverdx
Last active June 12, 2023 08:38
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save dfoverdx/2582340cab70cff83634c8d56b4417cd to your computer and use it in GitHub Desktop.
Save dfoverdx/2582340cab70cff83634c8d56b4417cd to your computer and use it in GitHub Desktop.
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 = { 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, 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 // => 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

  1. A bound function cannot be re-bound to a different this object. Calling bind() 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
    
  2. If you are using a compiler such as babel or TypeScript, you can enable decorators (babel/plugin-proposal-decorators, experimentalDecorators in tsconfig.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.)

@lambdabyte
Copy link

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
Copy link

CoinBR commented Jan 26, 2021

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

@aksperiod
Copy link

thank you for this!

@zdd
Copy link

zdd commented Jun 5, 2022

Excellent explanation! Thank you!

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