【问题标题】:Promise.then execution context when using class methods as callback使用类方法作为回调时的 Promise.then 执行上下文
【发布时间】:2017-06-03 05:32:11
【问题描述】:

为什么Promise.then 在使用类方法作为回调时传递undefined 的执行上下文,而在使用“普通函数”时传递window

类方法是否与其所属的对象/类分离?为什么是undefined 而不是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)

我预计this.method 的上下文会丢失,但无法理解为什么this.method 与“正常”和箭头函数之间的行为不同。

这种行为有规范吗?我发现的唯一参考是 Promises A+,它指的是 “在严格模式下 this 内部将是 undefined;在草率模式下,它将是 global object。”

【问题讨论】:

标签: javascript ecmascript-6 promise es6-promise


【解决方案1】:

你那里的报价告诉你原因:

在严格模式下,this 内部将是未定义的;在草率模式下,它将是全局对象。

ES6 spec 说:

ClassDeclaration 或 ClassExpression 的所有部分都是严格模式代码

因此,由于严格模式,未绑定类方法中的this 将是undefined

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

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

这与您在严格模式下传递普通函数的行为相同,因为this 绑定在严格模式下默认为undefined,而不是设置为全局对象。

normalarrow 具有 thiswindow 的原因是因为它们不在类中,因此未包装在严格模式中。


就承诺和then 方法而言,它只会将undefined 作为this 传递,但不会覆盖已经绑定的this

如果您查看PromiseReactionJob 规范:

带有参数 react 和 argument 的作业 PromiseReactionJob 将适当的处理程序应用于传入值,并使用处理程序的返回值来解析或拒绝与该句柄关联的派生承诺。

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

Call 的第二个参数是 this 值,设置为 undefined

【讨论】:

    【解决方案2】:

    这与 Promises 无关,而是调用 this 的上下文。

    案例 1:

    this.method(); // Foo
    

    这里methodFoo类中定义的函数,所以this被评估为触发函数的对象,即this.method中的this。因此 - Foo 会显示出来。

    案例 2:

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

    箭头函数是 ES6 的一个特性,其独特的属性是 this 上下文在定义它的封闭上下文中。该函数是在 this === Foo 的上下文中调用的,所以这就是显示的内容。

    案例 3:

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

    箭头函数将其上下文保留为窗口,因为它是一个箭头函数,并且普通函数在没有上下文的情况下进行评估,其中this 不在strict mode 中时被评估为窗口。

    案例 4:

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

    由于在窗口上声明的此函数的主体内请求了strict mode,因此this 被评估为未定义。

    案例 5:

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

    在这个spec中,定义了所有Class代码都是严格代码:

    ClassDeclaration 或 ClassExpression 的所有部分都是严格模式代码。

    【讨论】:

    • 除非我有误解,否则不清楚 OP 的代码是否是模块的一部分。
    • @nem035 正确,我想错了。已编辑,谢谢。
    • "在窗口上定义的函数" - 不,这不是normal 得到window 的原因
    • 那么处理这个问题的惯用语是什么?我希望在 Promise 的 then 中可以访问 React 类的外部范围。我可以创建 const that = this,但想知道是否有一些不那么混乱的东西。
    • @zero_cool 听起来你既可以在promise.then 回调中使用箭头函数,也可以在构造函数中创建一个回调并将其绑定到this
    【解决方案3】:

    就我而言,它有助于定义“自我”的简单解决方案。

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

    【讨论】:

      【解决方案4】:

      this.method 未定义的原因是因为当你这样使用它时,你实际上只是将没有上下文的函数作为回调。所以,当它运行时,它并不知道这一点。

      如果要维护上下文,请使用bind 函数。

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

      Bind 会将上下文绑定到方法。它本质上等同于:

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

      这是一个将上下文映射到作用域中的变量的包装器。

      对于一个类方法,当你把它作为一个变量来获取时,它本质上与一个包含对函数的引用的变量没有区别。

      例如:

      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
      

      当您在对象上调用方法时,例如foo.a(),它本质上与执行foo.a.call(foo) 相同,您将a 的上下文设置为foo。当你只取foo.a 并将其与foo 分开时,它与foo.a.call(window)(或Node 中的global)相同。

      下面是一些说明差异的代码。您还可以看到如果您 bind 它,它将如何保持上下文。

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

      【讨论】:

      • 你能解释更多“没有上下文”吗?为什么这与命名函数不同?
      猜你喜欢
      • 1970-01-01
      • 2012-11-12
      • 1970-01-01
      • 1970-01-01
      • 2015-12-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多