2

In the following code, I would expect printHello to be called twice (once from go() and once from runTask()):

class Hello {
  task = undefined;

  runTask() {
    console.log("Task is", this.task);
    this.task();
  }
  
  printHello() { 
    console.log("Hello"); 
  }
  
  go() {
    this.task = this.printHello;
    console.log("Set task to", this.task);
    setTimeout(this.runTask, 0); // Fails
    // setTimeout(() => this.runTask(), 0); // Works
  }
};

const hello = new Hello();
hello.go(); // Says `this.task` is undefined, but then...
hello.runTask(); // ... running it manually works fine

In my mind:

  • I create an instance of the class
  • Then calling go()
    • Sets the task field/property on that instance to printHello
    • Sets up a request to runTask after the timeout

I would expect that runTask would be called with task set to printHello, but it's actually undefined in the setTimeout call. However, if I just call runTask directly on my hello instance it works fine. Also, if I replace the setTimeout call with a lambda it works fine.

It seems clear it's a stale closure type of issue, I just don't have a great mental model or rulebook that would prevent me from writing code like this in the future and then be perplexed about why it wasn't working.

I was thinking that this would be captured when passing runTask, which should then point to a valid task, but that's not the case.

I'd really appreciate some insight about exactly why this doesn't work and requires the lambda version to be used.

aardvarkk
  • 14,955
  • 7
  • 67
  • 96
  • 1
    Do a small experiment, put a `console.log(this)` inside the `setTimeout`, that should print different output when you use the arrow notation than when you don't use it. – Alejandro Montilla Sep 03 '21 at 20:08
  • 4
    `setTimeout(this.runTask.bind(this), 0)` – Barmar Sep 03 '21 at 20:11
  • @Barmar That does seem to do the trick. But... How is it that a method on a class instance could lose its definition of `this`? I think I'm maybe bringing C++ baggage with me and being confused about how one could call a method on a class instance *without* having a valid `this` and needing to specify it. What's special about passing that method along that requires `bind` to be called like this? – aardvarkk Sep 03 '21 at 20:22
  • @SebastianSimon That does seem to touch on similar issues, but I wouldn't have expected this behaviour within a class instance. Since my `this` context does seem incorrect here, what `this` is being used in the `go()` version? – aardvarkk Sep 03 '21 at 20:24
  • 1
    `this` is only assigned when the method is *called*, not when it's referenced in other ways. – Barmar Sep 03 '21 at 20:25
  • @Barmar So without the bind, what is `this` referring to in this case? – aardvarkk Sep 03 '21 at 20:29
  • It gets the global object, i.e. `window` in a browser. – Barmar Sep 03 '21 at 20:30
  • @aar The behavior is unrelated to classes. `this` is `undefined` in strict mode or `globalThis` in loose mode when passed as a callback, unbound. Note that passing something as a parameter assigns it to the argument of the function which accepts the callback (`setTimeout` in this case). Since passing a parameter is a form of assignment, `const runTask = this.runTask; setTimeout(runTask);` would have the same result, but already at `const runTask`. (Note this other [linked question](/q/30486345/4642212)). See [How does the "this" keyword work?](/q/3127429/4642212), which explains everything. – Sebastian Simon Sep 03 '21 at 20:31
  • OK, I think that makes sense. If you'd like to submit a formal answer I'd be happy to mark it as solved. Thank you! – aardvarkk Sep 03 '21 at 20:31
  • @SebastianSimon You had provided [an earlier link](https://stackoverflow.com/questions/20279484/how-to-access-the-correct-this-inside-a-callback) which I think covered some of this material well. My concern with closing the question is that I would never have searched for the phrase 'How does the "this" keyword work?" in order to solve this particular issue. – aardvarkk Sep 03 '21 at 20:38
  • @aardvarkk _“I would never have searched for …”_ — Yes, I know that this is unfortunate… That’s why the duplicate target has [2,529 linked questions](/questions/linked/20279484). As [the fourth most linked JS question](/questions/tagged/javascript?tab=Frequent), it’s one of the most common issues that JS devs stumble upon sooner or later. On the other hand, reading the most frequent questions in this list is a great way to learn about the pitfalls of a language early. – Sebastian Simon Sep 03 '21 at 20:44

0 Answers0