admin管理员组

文章数量:1194114

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.".

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.".

Share Improve this question edited Jan 18, 2017 at 18:38 nem035 35.5k6 gold badges92 silver badges104 bronze badges asked Jan 18, 2017 at 17:52 SergioSergio 28.8k11 gold badges89 silver badges132 bronze badges 4
  • A class method reference passed as obj.method is always detached from obj - this isn't Promises specific. – Alnitak Commented Jan 18, 2017 at 18:06
  • @Alnitak but why undefined and not window as expected (jsFiddle)? – Sergio Commented Jan 18, 2017 at 18:07
  • 1 not sure - perhaps ES6 class methods are implicitly strict? – Alnitak Commented Jan 18, 2017 at 18:14
  • class methods are always strict mode. then passes nothing (undefined) as context, the rest is just the usual behavior of the this keyword. – Bergi Commented Jan 18, 2017 at 18:29
Add a comment  | 

4 Answers 4

Reset to default 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.

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.

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);
        }
      );
    });
  }
}

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);

本文标签: javascriptPromisethen execution context when using class methods as callbackStack Overflow