目前,我正在尝试在类构造函数中使用async/await。这样我就可以为我正在进行的Electron项目获得一个自定义的电子邮件标签。

customElements.define('e-mail', class extends HTMLElement {
  async constructor() {
    super()

    let uid = this.getAttribute('data-uid')
    let message = await grabUID(uid)

    const shadowRoot = this.attachShadow({mode: 'open'})
    shadowRoot.innerHTML = `
      <div id="email">A random email message has appeared. ${message}</div>
    `
  }
})

然而,目前项目不工作,有以下错误:

Class constructor may not be an async method

是否有一种方法来规避这一点,以便我可以使用异步/等待在这?而不是要求回调或.then()?


当前回答

这是行不通的。

async关键字允许await在标记为async的函数中使用,但它也将该函数转换为promise生成器。因此,标记为async的函数将返回一个promise。另一方面,构造函数返回它正在构造的对象。因此,我们有这样一种情况,您希望同时返回一个对象和一个承诺:这是不可能的情况。

你只能在可以使用承诺的地方使用async/await,因为它们本质上是承诺的语法糖。不能在构造函数中使用promise,因为构造函数必须返回要构造的对象,而不是promise。

有两种设计模式可以克服这个问题,它们都是在承诺出现之前发明的。

使用init()函数。这有点像jQuery的.ready()。你创建的对象只能在它自己的init或ready函数中使用:

用法:

    var myObj = new myClass();
    myObj.init(function() {
        // inside here you can use myObj
    });

实现:

    class myClass {
        constructor () {

        }

        init (callback) {
            // do something async and call the callback:
            callback.bind(this)();
        }
    }

使用构建器。我在javascript中没有见过这种方法,但当需要异步构造对象时,这是Java中更常见的解决方法之一。当然,在构造需要大量复杂参数的对象时,会使用构建器模式。这正是异步构建器的用例。区别在于异步构建器不返回对象,而是返回该对象的承诺:

用法:

    myClass.build().then(function(myObj) {
        // myObj is returned by the promise, 
        // not by the constructor
        // or builder
    });

    // with async/await:

    async function foo () {
        var myObj = await myClass.build();
    }

实现:

    class myClass {
        constructor (async_param) {
            if (typeof async_param === 'undefined') {
                throw new Error('Cannot be called directly');
            }
        }

        static build () {
            return doSomeAsyncStuff()
               .then(function(async_result){
                   return new myClass(async_result);
               });
        }
    }

使用async/await实现:

    class myClass {
        constructor (async_param) {
            if (typeof async_param === 'undefined') {
                throw new Error('Cannot be called directly');
            }
        }

        static async build () {
            var async_result = await doSomeAsyncStuff();
            return new myClass(async_result);
        }
    }

注意:虽然在上面的例子中,我们为异步构建器使用了promise,但严格来说它们并不是必需的。您也可以轻松地编写一个接受回调的构建器。


注意在静态函数中调用函数。

这与异步构造函数没有任何关系,而是与关键字This的实际含义有关(对于来自自动解析方法名的语言的人来说,这可能有点令人惊讶,即不需要This关键字的语言)。

this关键字引用实例化的对象。不是这门课。因此,你通常不能在静态函数内部使用它,因为静态函数不绑定到任何对象,而是直接绑定到类。

也就是说,在下面的代码中:

class A {
    static foo () {}
}

你不能:

var a = new A();
a.foo() // NOPE!!

相反,你需要调用它为:

A.foo();

因此,下面的代码将导致错误:

class A {
    static foo () {
        this.bar(); // you are calling this as static
                    // so bar is undefinned
    }
    bar () {}
}

要修复它,你可以将bar设置为常规函数或静态方法:

function bar1 () {}

class A {
    static foo () {
        bar1();   // this is OK
        A.bar2(); // this is OK
    }

    static bar2 () {}
}

其他回答

权宜之计

你可以创建一个async init(){…返回this;}方法,然后执行new MyClass().init(),当你通常只说new MyClass()时。

这并不干净,因为它依赖于使用您的代码的每个人,以及您自己,总是像这样实例化对象。但是,如果您只在代码中的一两个特定位置使用该对象,则可能没有问题。

但是一个重要的问题出现了,因为ES没有类型系统,所以如果你忘记调用它,你只是返回了undefined,因为构造函数没有返回任何东西。哦。更好的做法是这样做:

最好的办法是:

class AsyncOnlyObject {
    constructor() {
    }
    async init() {
        this.someField = await this.calculateStuff();
    }

    async calculateStuff() {
        return 5;
    }
}

async function newAsync_AsyncOnlyObject() {
    return await new AsyncOnlyObject().init();
}

newAsync_AsyncOnlyObject().then(console.log);
// output: AsyncOnlyObject {someField: 5}

工厂方法解决方案(稍好)

然而,你可能会意外地做新的AsyncOnlyObject,你应该直接创建使用Object.create(AsyncOnlyObject.prototype)的工厂函数:

async function newAsync_AsyncOnlyObject() {
    return await Object.create(AsyncOnlyObject.prototype).init();
}

newAsync_AsyncOnlyObject().then(console.log);
// output: AsyncOnlyObject {someField: 5}

However say you want to use this pattern on many objects... you could abstract this as a decorator or something you (verbosely, ugh) call after defining like postProcess_makeAsyncInit(AsyncOnlyObject), but here I'm going to use extends because it sort of fits into subclass semantics (subclasses are parent class + extra, in that they should obey the design contract of the parent class, and may do additional things; an async subclass would be strange if the parent wasn't also async, because it could not be initialized the same way):


抽象解决方案(扩展/子类版本)

class AsyncObject {
    constructor() {
        throw new Error('classes descended from AsyncObject must be initialized as (await) TheClassName.anew(), rather than new TheClassName()');
    }

    static async anew(...args) {
        var R = Object.create(this.prototype);
        R.init(...args);
        return R;
    }
}

class MyObject extends AsyncObject {
    async init(x, y=5) {
        this.x = x;
        this.y = y;
        // bonus: we need not return 'this'
    }
}

MyObject.anew('x').then(console.log);
// output: MyObject {x: "x", y: 5}

(不要在生产中使用:我没有考虑过复杂的场景,比如这是否是为关键字参数编写包装器的正确方式。)

不像其他人说的,你可以让它工作。

JavaScript类可以从它们的构造函数返回任何东西,甚至是另一个类的实例。因此,您可以从解析到其实际实例的类的构造函数返回一个Promise。

下面是一个例子:

export class Foo {

    constructor() {

        return (async () => {

            // await anything you want

            return this; // Return the newly-created instance
        })();
    }
}

然后,你将这样创建Foo的实例:

const foo = await new Foo();

您可以立即调用一个返回消息的匿名异步函数,并将其设置为message变量。如果您不熟悉这种模式,您可能想看看立即调用的函数表达式(IEFES)。这将非常有效。

var message = (async function() { return await grabUID(uid) })()

我通常更喜欢返回一个新实例的静态异步方法,但这里有另一种方法。它更接近于字面上的等待构造函数。它与TypeScript一起工作。

class Foo {
  #promiseReady;

  constructor() {
    this.#promiseReady = this.#init();
  }

  async #init() {
    await someAsyncStuff();
    return this;

  }

  ready() {
    return this.promiseReady;
  }
}
let foo = await new Foo().ready();

其他答案都忽略了一个显而易见的事实。简单地从你的构造函数调用一个async函数:

constructor() {
    setContentAsync();
}

async setContentAsync() {
    let uid = this.getAttribute('data-uid')
    let message = await grabUID(uid)

    const shadowRoot = this.attachShadow({mode: 'open'})
    shadowRoot.innerHTML = `
      <div id="email">A random email message has appeared. ${message}</div>
    `
}