0

Duplication Notes

Problem

When I use classes to define object: when I use the object's method as callback the this pointer does not point the object itself anymore.

Example

class MyClass {
  bar = 'my bar';

  foo() {
    console.log(this.bar);
  }
}

function runCallback(func) {
  return func();
}

const myClass = new MyClass();


// Output: 'my bar' (as expected)
myClass.foo()

// Output: 'undefined' (unexpected)
runCallback(myClass.foo)

Question

How can I make Javascript use the correct value of this (= the object the method belongs to).

Javascript knowledge

The Problem happens, because this in Javascript means something different than in other languages.

DarkTrick
  • 2,447
  • 1
  • 21
  • 39
  • 2
    I feel like people could just get this from [How does `this` work in JavaScript?](https://stackoverflow.com/questions/3127429/how-does-the-this-keyword-work-and-when-should-it-be-used) if they bother to scroll down just a little. – kelsny Nov 09 '22 at 04:48
  • 1
    @caTS Accepted answer in that post is missing TL;DR. But I guess most people who come to SO are looking for a way to get around their problem and not to get in-depth understanding. For those few souls, that answer will do the trick – Rajesh Nov 09 '22 at 05:29
  • 1
    Duplicate notes: classes aren't really any special case, and answers in the duplicate do talk about the class case as well. – deceze Nov 09 '22 at 07:00
  • @deceze Thank you for the comment. Classes might be a special case (unless proven otherwise; AFAIK classes have their own implementation in JS, which indeed would make them a special case). `Duplication` notice is about questions, not about answers. If there are class related answers in the other question, it would make them off-topic over there (Maybe the question focus in the other question is too narrow and should be broadened?). – DarkTrick Nov 09 '22 at 07:20
  • Let's put it this way: this is one of the most common questions in [tag:javascript], and that duplicate is the canonical duplicate, and so far the need to address classes in a separate question hasn't come up… – deceze Nov 09 '22 at 07:23

1 Answers1

2
  • This answer is a community Wiki. Please add further ways of dealing with the problem as you find them.

Solutions without salt grains

None as of now

Accept, that Javascript does not support classes in the common way.

Solutions that come with a grain of salt

Use arrow-functions on callback

runCallback((...params) => myClass.foo(...params))

Use bind on methods when used as callback

runCallback(myClass.foo.bind(myClass))

Why does it work?
this for foo gets specifically set to myClass

Problems:

  • Fragility: Might be easy to forget binding.
  • Blackbox principle break: The correctness of method foo will depend on how it is called, not how it is defined (however, note that this was the case to begin with)
  • Readability: Code gets longer

Use bind on methods during creation

class MyClass {
  bar = 'my bar';

  constructor(){
    this.foo = this.foo.bind(this);
  }

  foo() {
    console.log(this.bar);
  }
}

Reference: https://stackoverflow.com/questions/56503531/what-is-a-good-way-to-automatically-bind-js-class-methods

Problems:

  • Maintenance: You need bind methods inside the constructor
  • Fragility: It's easy to forget binding on (new) methods
  • Memory: Functions will turn to per-instance functions (i.e. each object has their own function object)

use auto-bind

Available as library, or implement it yourself (see below)

class MyClass {
  bar = 'my bar';
  
  constructor() {
    autoBind(this);
  }
    
  foo() {
    return String(this.bar);
  }
}

Problems:

  • Memory: Functions will turn to per-instance functions (i.e. each object has their own function object)
  • Fragility: Don't forget to call autoBind

Implemenation of autoBind:

/**
 *  Gets all non-builtin properties up the prototype chain.
 **/
const _getAllProperties = (object) => {
    const properties: any = [];

    do {
        for (const key of Reflect.ownKeys(object)) {
            properties.push({ obj: object, key: key });
        }
    } while ((object = Reflect.getPrototypeOf(object)) && object !== Object.prototype);

    return properties;
};

function autoBind(self) {
    const props = _getAllProperties(self.constructor.prototype);
    props.forEach((prop) => {
        if (prop.key === 'constructor') {
            return;
        }

        const descriptor = Reflect.getOwnPropertyDescriptor(prop.obj, prop.key);
        if (descriptor && typeof descriptor.value === 'function') {
            self[prop.key] = self[prop.key].bind(self);
        }
    });
    return self;
}

Use fields and Arrow functions

class MyClass3 implements MyInterface {
  bar = 'my bar';

  foo = () => {
    console.log(this.bar);
  };
}

Why does it work?
Arrow functions are automatically bound (reference)

Problems:

  • Problems with arrow functions in class fields
    • Not Mockable
    • Inheritance won't work as expected
    • Memory: Functions will turn to per-instance functions (i.e. each object has their own function object)function implementation (as opposed to one implemenation for all objects).
DarkTrick
  • 2,447
  • 1
  • 21
  • 39
  • An extended example of `.bind` approach: [JSFiddle](https://jsfiddle.net/sv401j63/) with an addition of possibility of passing context from caller. – Rajesh Nov 09 '22 at 05:26
  • 1
    1. This fails to discuss one important difference of binding the method in the class (#1, #3, #4) - this means there are as many methods in memory as there are instances. Auto-binding or otherwise using these techniques on each method can lead to memory bloat when creating many instances. It might not be an issue if there is, say, only one or just a few instances created but just making all methods bound should be used sparingly. 2. One very easy approach to solve the entire problem without changing the entire class is `runCallback(() => myClass.foo())` – VLAZ Nov 09 '22 at 05:33
  • @VLAZ (1) Are you talking about the `bind` approach here? Regarding the field/arrow approach this is stated (2) Your neglecting parameters. But the option should be added. – DarkTrick Nov 09 '22 at 05:38
  • 1
    For 2. I didn't. I just used the example you gave in the question. It didn't have parameters. I hoped I didn't have to expand because it seemed trivial that if you *do* have parameters, you can just pass them in: `runCallback((param1, param2) => myClass.foo(param1, param2))`. After all `arr.map((x) => myFn(x))` isn't fairly well established idiom, as well. I thought it's a waste of comment space to explain how parameters can be used in a callback. At best a brief reminder about `runCallback((...args) => myClass.foo(...args))` might be OK but again - comment space. – VLAZ Nov 09 '22 at 05:56
  • 2
    For 1. All of #1, #3, and #4 - `autoBind` would make all methods instance-bound, thus there would be one method per instance. Also `class Foo { bar = () => {} }` will make the method instance-bound. If the same technique is applied to *all* methods, even if not needed, then a class with, say, 10 methods which is instantiated 1000 times, will have 10000 methods floating around in memory. – VLAZ Nov 09 '22 at 05:56
  • For me, the obvious choice would also be an arrow function at the call site. – CherryDT Nov 09 '22 at 07:06
  • @VLAZ Added memory information and arrow function. Thank you! – DarkTrick Nov 09 '22 at 07:41