12

Why does Promise.then passes execution context of undefined when using a class method as callback, and window when using a "normal function"?

Is the class method detached from its owning object/class? and why undefined and not window?

function normal() {
    console.log('normal function', this);
}
const arrow = () => {
    console.log('arrow function', this);
}

function strictFunction() {
    'use strict';
    console.log('strict function', this);
}

class Foo {
    test() {
        this.method(); // Foo
        Promise.resolve().then(() => console.log('inline arrow function', this)); // Foo
        Promise.resolve().then(normal); // window
        Promise.resolve().then(arrow); // window
        Promise.resolve().then(strictFunction); // undefined
        Promise.resolve().then(this.method); // undefined <-- why?
    }

    method() {
        console.log('method', this);
    }
}

const F = new Foo();
F.test();

(jsFiddle)

I would expect the context of this.method to be lost but cannot understand why the different behavior between this.method and "normal" and arrow functions.

Is there a spec for this behavior? The only reference I found was Promises A+ refering to that "in strict mode this will be undefined inside; in sloppy mode, it will be the global object.".

nem035
  • 34,790
  • 6
  • 87
  • 99
Sergio
  • 28,539
  • 11
  • 85
  • 132
  • A class method reference passed as `obj.method` is _always_ detached from `obj` - this isn't Promises specific. – Alnitak Jan 18 '17 at 18:06
  • @Alnitak but why `undefined` and not `window` [as expected (jsFiddle)](https://jsfiddle.net/9x47jahe/)? – Sergio Jan 18 '17 at 18:07
  • 1
    not sure - perhaps ES6 class methods are implicitly strict? – Alnitak Jan 18 '17 at 18:14
  • [`class` methods are always strict mode](http://stackoverflow.com/q/29283935/1048572). `then` passes nothing (`undefined`) as context, the rest is just the [usual behavior of the `this` keyword](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this). – Bergi Jan 18 '17 at 18:29

4 Answers4

13

The quote you have there tells you why:

in strict mode this will be undefined inside; in sloppy mode, it will be the global object.

The ES6 spec says that:

All parts of a ClassDeclaration or a ClassExpression are strict mode code

Therefore, because of strict mode, this within an unbound class method, will be undefined.

class A {
  method() {
    console.log(this);
  }
}

const a = new A();
a.method(); // A
const unboundMethod = a.method;
unboundMethod(); // undefined

This is the same behavior you would get if you passed a normal function with strict mode because this binding is undefined by default in strict mode, not set to the global object.

The reason normal and arrow have this as window is because they are not within the class and thus not wrapped in strict mode.


As far as promises and the then method, it will just pass undefined as this but won't override already bound this.

If you look at the PromiseReactionJob spec:

The job PromiseReactionJob with parameters reaction and argument applies the appropriate handler to the incoming value, and uses the handler's return value to resolve or reject the derived promise associated with that handle.

...
let handlerResult be Call(handler, undefined, «argument»).

The second argument to Call is the this value, which is set to undefined.

Sergio
  • 28,539
  • 11
  • 85
  • 132
nem035
  • 34,790
  • 6
  • 87
  • 99
6

This has nothing to do with Promises, but rather the context in which this is called.

Case 1:

this.method(); // Foo

Here method is a function defined within the Foo class, so this is evaluated as the object that triggered the function, which is this in this.method. Hence - Foo is displayed.

Case 2:

Promise.resolve().then(() => console.log('inline arrow function', this)); // Foo

Arrow functions are a feature of ES6, whose unique property is that the this context in the enclosing context where it is defined. The function was called in a context where this === Foo so that's what's displayed.

Case 3:

Promise.resolve().then(normal); // window
Promise.resolve().then(arrow); // window

The arrow function retains its context as the window since it's an arrow function, and the normal function is evaluated without a context, in which this is evaluated to window when not in strict mode.

Case 4:

Promise.resolve().then(strictFunction); // undefined

Since strict mode is requested within the body of this function, which is declared on the window, this is evaluated to undefined.

Case 5:

Promise.resolve().then(this.method); // undefined <-- why?

In this spec, it is defined that all Class code is strict code:

All parts of a ClassDeclaration or a ClassExpression are strict mode code.

Omri Aharon
  • 16,959
  • 5
  • 40
  • 58
  • Unless I'm misunderstanding something, it is not clear if OP's code is part of a module. – nem035 Jan 18 '17 at 18:22
  • 1
    @nem035 Correct, I had the wrong quote in mind. Edited, thanks. – Omri Aharon Jan 18 '17 at 18:24
  • "*functions that are defined on the window*" - no, that's not the reason why `normal` gets `window` – Bergi Jan 18 '17 at 18:30
  • What is the idiomatic to handle this then? I'd like the outer scope of a React class to be accessible in the then of a Promise. I can create const that = this, but wondering if there is something less messy. – zero_cool Oct 30 '18 at 20:18
  • @zero_cool Sounds like you can either use an arrow function in the `promise.then` callback, or create a callback and bind it to `this` in the constructor. – Omri Aharon Oct 31 '18 at 06:38
2

In my case it helped the simple solution of defining "self".

app.component.ts

export class AppComponent implements OnInit {
  public cards: Card[] = [];
  public events: any[] = [];

  constructor(private fbService: FacebookService) {
    this.fbService.loadSdk();
  }

  ngOnInit() {
    const self = this;

    this.fbService.getEvents().then((json: any) => {
      for (const event of json.data)
      {
        self.cards.push(
          new Card({
            imageUrl: 'assets/ny.jpg',
            id: event.id,
            name: event.name
          }),
        );
      }
    });
  }
}

fb.service.ts

import { BehaviorSubject } from 'rxjs/Rx';
import { Injectable, NgZone } from '@angular/core';
import { Http } from '@angular/http';


declare var window: any;
declare var FB: any;

@Injectable()
export class FacebookService {
  events: any[];

  public ready = new BehaviorSubject<boolean>(false);

  constructor(private zone: NgZone) {
  }

  public loadSdk() {
    this.loadAsync(() => { });
  }


  public loadAsync(callback: () => void) {
    window.fbAsyncInit = () => this.zone.run(callback);
    // Load the Facebook SDK asynchronously
    const s = 'script';
    const id = 'facebook-jssdk';
    const fjs = document.getElementsByTagName(s)[0];
    // tslint:disable-next-line:curly
    if (document.getElementById(id)) return;

    const js = document.createElement(s);
    js.id = id;
    js.src = 'http://connect.facebook.net/en_US/all.js';
    fjs.parentNode.insertBefore(js, fjs);
  }

  public getEvents(): Promise<any> {
    return new Promise((resolve, reject) => {
      FB.init({
        appId: 'app_id',
        xfbml: true,
        status: true, 
        cookie: true,
        version: 'v2.10'
      });

      FB.api(
        '/salsaparty.bg/events',
        'GET',
        {
          access_token: 'acess_token'
        },
        function (response) {
          resolve(response);
        }
      );
    });
  }
}
Zdravko Kolev
  • 1,569
  • 14
  • 20
0

The reason this.method is undefined is because when you use it like that, you are actually just taking the function, devoid of context, as the callback. So, when it's run, it doesn't know this.

If you want to maintain context, use the bind function.

Promise.resolve().then(this.method.bind(this))

Bind will bind the context to the method. It's essentially equivalent to this:

Promise.resolve().then(((self) => () => self.method())(this))

which is a wrapper to map the context to a variable in a scope.

With a class method, when you get it as a variable, it's essentially no difference from a variable containing a reference to a function.

For example:

const a = () => {};

class Foo {
    a() {}
}
const foo = new Foo();

console.log(a); // just a function
console.log(foo.a) // just a function
console.log(foo.a()) // a function called with a context of foo

When you call a method on an object, like foo.a() it's essentially the same as doing foo.a.call(foo), where you set the context of a to foo. When you just take foo.a and separate it from foo, it's the same as doing foo.a.call(window) (or global in Node).

Here is some code that illustrates the differences. You can also see how if you bind it, it'll maintain context.

class Foo {
  constructor() {
    this.b = this.b.bind(this);
  }
  
  a () {
    return this;
  }
  
  b () {
    return this;
  }
}

const foo = new Foo();
const a = foo.a;
const b = foo.b;
const bound = foo.a.bind(foo);

console.log('A', foo.a().constructor.name);
console.log('A', a());
console.log('A', a.apply(foo).constructor.name);
console.log('A', bound().constructor.name);

console.log('B', foo.b().constructor.name);
console.log('B', b().constructor.name);
samanime
  • 25,408
  • 15
  • 90
  • 139