0

I am creating a library and one of the patterns I need to implement is event handling. I am writing in TypeScript, but this issue is basically in Javascript. Consider this code:

class MessageRelayer {
    private readonly listeners: Array<(args: string) => void> = [];

    public addListener(listener: (args: string) => void): void {
        this.listeners.push(listener);
    }

    public relayMessage(msg: string): void {
        for (const listener of this.listeners) listener(msg);
    }
}

class TestClass {
    private el: string;

    constructor(private readonly relay: MessageRelayer) {
        this.el = "example";
    }

    public initialize(): void {
        this.relay.addListener(this.onRelayed);
    }

    private onRelayed(e: string): void {
        console.log("EL is:", this.el);
    }
}

const relay = new MessageRelayer();
const stuff = new TestClass(relay);
stuff.initialize();

relay.relayMessage("Hello world"); // Error here

When running this code, it gives me error in onRelayed:

Cannot read property 'el' of undefined

If initialize is written as:

public initialize(): void {
    this.relay.addListener((e: string) => { console.log("EL is:", this.el); });
}

It works.

Questions

I understand this is about the famous this binding issue. But what's happening is not something I can explain here. What I think it should happen is:

  1. When calling initialize(), function TestClass.onRelayed is passed to MessageRelayer. It means that in that moment, this for TestClass.onRelayed will be bound to MessageRelayer.
  2. As soon as relayMessage is invoked, MessageRelayer will get the saved instance of TestClass.onRelayed whose this is now pointing to MessageRelayer, and executed it.
  3. I should get an error that this does not contain el.

Instead I find out that this is actually undefined. What is happening here?

Andry
  • 14,281
  • 23
  • 124
  • 216
  • 1
    "*When calling initialize(), function TestClass.onRelayed is passed to MessageRelayer. It means that in that moment, this for TestClass.onRelayed will be bound to MessageRelayer.*" the value of `this` is determined *at call time*, not before that. So when you call `initialize`, you pass a function reference as the argument to `addListener` as `this.onRelayed` and disassociate it permanently from the `this` context, since when you call it it's just `listener()` - at *that* point there is not context for it. – VLAZ Jul 26 '20 at 21:09
  • They do not get `undefined` there, but the solution might be applicable. I would like to understand why I get `undefined here`. I am reading the question and the answer, if this is the same exact problem, I will delete the question. – Andry Jul 26 '20 at 21:09
  • 1
    Here is a rule of thumb *when you call* a function, `this` is set to whatever was before the last dot. So, if you *call* `a.b.c.foo()` then `this = a.b.c`. If you call `a.b.foo()` then `this = a.b`. If you call `a.foo()` then `this = a`. But if you call `foo()` then (to shorten it) `this = undefined`. If you do `bar = a.b.c.foo; bar()` then you are *not* calling with `a.b.c` as context, therefore `this = undefined`. See also [How does the “this” keyword work?](https://stackoverflow.com/q/3127429) – VLAZ Jul 26 '20 at 21:12
  • @VLAZ: Please post an answer, it seems to address the specific issue I am reporting here. – Andry Jul 26 '20 at 21:12
  • I see that the question you posted actually is the same, they are also getting `undefined`. I am voting to close as well. – Andry Jul 26 '20 at 21:16

0 Answers0