5

The code sample is -

global.a = 'aaa';

const obj = {
    a: 'a',
    desc() {
        console.log(this);
        console.log(this.a);
    }
}

setTimeout(obj.desc, 2000)

When I run this code in nodejs, I get the following output:

Timeout {
  _called: true,
  _idleTimeout: 2000,
  _idlePrev: null,
  _idleNext: null,
  _idleStart: 79,
  _onTimeout: [Function: desc],
  _timerArgs: undefined,
  _repeat: null,
  _destroyed: false,
  [Symbol(asyncId)]: 6,
  [Symbol(triggerAsyncId)]: 1 }
undefined

But the same code, with global changed to window in Chrome/Firefox prints aaa and the window object, which is what this MDN doc says and which is what I expect.

I was under the impression that nodejs and Chrome both use Google's v8 JS engine to execute JavaScript. So why is the output different? Is there something more to this? I tried searching but couldn't find satisfactory answers.

Node version - v9.10.
Chrome's version - Version 70.0.3538.110

arfat
  • 136
  • 1
  • 6
  • 1
    Possible duplicate of [What is the 'global' object in NodeJS](https://stackoverflow.com/questions/43627622/what-is-the-global-object-in-nodejs) – Herohtar Nov 28 '18 at 22:49
  • [Timers](https://nodejs.org/api/timers.html) are a very different beast under node.js. That both programs use the same JS engine doesn't mean they have to provide the same IO functionality. – Bergi Nov 28 '18 at 22:53
  • Might be a duplicate of https://stackoverflow.com/questions/24653995/node-js-settimeout-behaviour Hope it helps! – Émile Bernard Nov 28 '18 at 22:53

2 Answers2

3

node.js has its own implentation of timers which is different from browser implementations (although they can generally be used in the same way). This isn't really documented, but when you use setTimeout, it creates an instance of a Timer class with the callback passed as an argument. This callback is assigned to a property of the Timer instance which is used later:

This means that the this context for timers in node.js incidentally gets set to the Timer instance itself. Browsers apparently do something different.

This is mostly a semantic issue in your original code: when you pass function properties from objects they lose their context. You could use .bind to retain the context, or use another function to call desc directly on obj to retain the context.

setTimeout(obj.desc.bind(obj), 2000);
setTimeout(() => obj.desc(), 2000);

These two will log obj and obj.a in both node.js and browser environments.

Explosion Pills
  • 176,581
  • 46
  • 285
  • 363
1

To answer your question, we must consult the source code for timers, since NodeJS' setTimeout and vanilla JS' setTimeout are not the same thing.

If we look here, we will find the definition for setTimeout. We must pay attention to what happens with the callback that is passed in:

What happens is it is passed into the Timeout constructor:

const timeout = new Timeout(callback, after, args, false);

Ok, so what is the Timeout class? We can find that here. Notice the line:

this._onTimeout = callback;

Ok, we stick the callback in an instance member, so I suspect that's how it is being called.

If we go back to the other source file and search for _onTimeout, we will find this line:

timer._onTimeout();

So in this case, because of how the callback is being called, timer is in fact what this refers to (as you've noted by logging this from the callback), and since the timer (or Timeout instance) does not have any such property (a), it logs undefined.

In vanilla JS, it behaves differently, and this refers to the window in your case.

As you may know, you can resolve this inconsistency by simply binding the callback to obj in setTimeout like so:

setTimeout(obj.desc.bind(obj), 2000);
pushkin
  • 7,514
  • 14
  • 41
  • 74