【问题标题】:Extend Javascript promise and resolve or reject it inside constructor扩展 Javascript 承诺并在构造函数中解决或拒绝它
【发布时间】:2018-06-17 22:18:51
【问题描述】:

我想用 ES6 语法扩展原生 Javascript Promise 类,并且能够在子类构造函数中调用一些异步函数。根据异步函数结果,promise 必须被拒绝或解决。

然而,当then函数被调用时,会发生两件奇怪的事情:

  1. 子类构造函数执行两次
  2. “Uncaught TypeError: Promise resolve or reject function is not callable”错误抛出

    class MyPromise extends Promise {
        constructor(name) {
            super((resolve, reject) => {
                setTimeout(() => {
                    resolve(1)
                }, 1000)
            })

            this.name = name
        }
    }

    new MyPromise('p1')
        .then(result => {
            console.log('resolved, result: ', result)
        })
        .catch(err => {
            console.error('err: ', err)
        })

【问题讨论】:

标签: javascript inheritance es6-promise es6-class


【解决方案1】:

您必须通过实现then 方法使其thenable

否则,将调用超类 Promise 的超类,并尝试使用您的 MyPromise' 构造函数创建另一个 Promise,这与原始 Promise 构造函数不兼容。

问题是,正确实现 then 方法很棘手,就像 Promise 一样。您最终可能会将 Promise 的实例作为成员,而不是作为超类。

【讨论】:

    【解决方案2】:

    推理很简单,但不一定是不言而喻的。

    • .then() 返回一个承诺
    • 如果在 Promise 的子类上调用 then,则返回的 Promise 是子类的实例,而不是 Promise 本身。
    • then 返回的 Promise 是通过调用子类构造函数构造的,并向其传递一个内部执行函数,该函数记录传递给它的 resolvereject 参数的值以供以后使用。
    • “稍后使用”包括在监视 onfulfilledonrejected 处理程序(稍后)的执行以查看它们是否返回值(这解决了 then 返回的承诺时)异步解决或拒绝 then 返回的承诺) 或抛出错误(拒绝承诺)。

    简而言之,then 调用在内部获取并记录对它们返回的 Promise 的 resolvereject 函数的引用。


    所以关于这个问题,
    new MyPromise( 'p1')
    

    工作正常,是对子类构造函数的第一次调用。

    .then( someFunction)
    

    someFunction 记录在对new MyPromise 进行的then 调用列表中(调用then 可以多次调用)并尝试通过调用来创建返回承诺

    new MyPromise( (resolve, reject) => ... /* store resolve reject references */
    

    这是来自then 代码的对子类构造函数的第二次调用。构造函数应该(并且确实)同步返回。

    在从创建返回的承诺返回时,.then 方法会进行完整性检查,以查看它需要供以后使用的 resolvereject 函数是否实际上是函数。它们应该与then 调用中提供的回调一起存储(在列表中)。

    MyPromise 的情况下,它们不是。通过then 传递给MyPromise 的执行程序甚至没有被调用。所以then 方法代码会抛出一个类型错误“Promise resolve or reject function is not callable”——它没有办法解决或拒绝它应该返回的 Promise。

    在创建 Promise 的子类时,子类构造函数必须将执行器函数作为其第一个参数,并使用真正的 resolvereject 函数参数调用执行器。这是then 方法代码内部需要的。

    使用MyPromise 做一些复杂的事情,也许检查第一个参数以查看它是否是一个函数,如果是,则将其作为执行程序调用,可能是可行的,但超出了此答案的范围!对于显示的代码,编写工厂/库函数可能更简单:

    function namedDelay(name, delay=1000, value=1) {
         var promise = new Promise( (resolve,reject) => {
             setTimeout(() => {
                    resolve(value)
                }, delay)
             }
         );
        promise.name = name;
        return promise;
    }
    
    namedDelay( 'p1')
        .then(result => {
            console.log('fulfilled, result: ', result)
        })
        .catch(err => {
            console.error('err: ', err)
        })

    ;TLDR

    Promise 的类扩展不是扩展。如果是,则需要实现 Promise 接口并将执行器函数作为第一个参数。您可以使用工厂函数返回异步解析的 Promise(如上),或者使用 hack 发布的代码

    MyPromise.prototype.constructor = Promise
    

    这会导致.then 返回一个常规的 Promise 对象。 hack 本身驳斥了正在发生类扩展的想法。


    Promise 扩展示例

    以下示例显示了一个基本的 Promise 扩展,它添加了提供给构造函数的属性。注意:

    • Symbol.toString getter 仅影响将实例转换为字符串的输出。在测试的浏览器控制台上记录实例 object 时,它不会将“Promise”更改为“MyPromise”。

    • Firefox 89 (Proton) 没有报告扩展实例的自身属性,而 Chrome 报告了 - 下面的测试代码按名称记录实例属性的原因。

    class MyPromise extends Promise {
        constructor(exec, props) {
            if( typeof exec != "function") {
                throw TypeError( "new MyPromise(executor, props): an executor function is required");
            }
            super((resolve, reject) => exec(resolve,reject));
            if( props) {
                Object.assign( this, props);
            } 
        }
        get [Symbol.toStringTag]() {
            return 'MyPromise';
        }
    }
    
    // Test the extension:
    const p1 = new MyPromise( (resolve, reject) =>
        resolve(42),
        {id: "p1", bark: ()=>console.log("woof") });
    
    console.log( "p1 is a %s object", p1.constructor.name);
    console.log( "p1.toString() = %s", p1.toString());
    console.log( "p1.id = '%s'", p1.id);
    console.log( "p1 says:"); p1.bark();
    
    const pThen = p1.then(data=>data);
    console.log( "p1.then() returns a %s object", pThen.constructor.name);
    let pAll = MyPromise.all([Promise.resolve(39)]);
    console.log( "MyPromise.all returns a %s object", pAll.constructor.name);
    try { new MyPromise(); }
    catch(err) {
        console.log( "new MyPromise() threw: '%s'", err.message);
    }

    【讨论】:

    • 感谢@traktor53 提供完整的逻辑描述。我猜像jsfiddle.net/p7b6gaqd/15 这样的东西应该也能正常工作?
    • @Soul_man 代码似乎正朝着正确的方向发展,但如前所述,“超出了此答案的范围”。鉴于 cmets 不是扩展现有问题的地方,如果您需要更多帮助和/或反馈,请在此处或 Code Review 提出新问题。它也让其他人有机会回答:-)
    • 所以,因为 MyPromise 的构造函数,而不是 Promise 的构造函数,用于构造 派生 Promises,就像 Promise 的构造函数一样可以,你必须运行给定的执行程序(如果有的话),并在MyPromise 的构造函数中正确地提供你从超类Promise 获得的resolvereject 函数。好的,我想我明白了。
    【解决方案3】:

    asdru 的帖子包含正确答案,但也包含应不鼓励的方法(构造函数破解)。

    构造函数 hack 检查构造函数参数是否为函数。这不是要走的路,因为 ECMAScript 设计包含通过 Symbol.species 对 Promises 进行子类化的特定机制。

    asdru 对使用Symbol.species 的评论是正确的。见当前ECMAScript specification中的解释:

    Promise 原型方法通常使用其 this 值的构造函数 创建派生对象。但是,子类构造函数可能 通过重新定义其 @@species 属性来覆盖该默认行为。

    规范(间接)在finallythen 部分中引用了此注释(查找提及SpeciesConstructor)。

    通过返回Promise 作为物种构造函数,避免了traktor 的答案分析得如此清楚的问题。 then 调用 Promise 构造函数,但不调用子类 MyPromise 构造函数。 MyPromise 构造函数只使用name 参数调用一次,不需要或不适当的进一步的参数检查逻辑。

    因此,代码应该是:

    class MyPromise extends Promise {
        constructor(name) {
            super((resolve, reject) => {
                setTimeout(() => {
                    resolve(1)
                }, 1000)
            })
            this.name = name
        }
    
        static get [Symbol.species]() {
            return Promise;
        }
    
        get [Symbol.toStringTag]() {
            return 'MyPromise';
        }
    }
    

    少即是多!

    一些注意事项:

    • MDN 有一个使用物种符号扩展Array 的示例。

    • 最新的浏览器版本(Chrome、FF、Safari、Mac 和 Linux 上的 Edge)可以正确处理此问题,但我没有关于其他浏览器或旧版本的信息。

    • Symbol.toStringTag 是一个非常好的触摸,但不是必需的。大多数浏览器使用此符号返回的值来识别控制台中的子类承诺,但请注意,FF 不会 - 这很容易造成混淆。然而,在所有浏览器中,new MyPromise('mine').toString() 产生 "[object MyPromise]"

    • 如果您在 Typescript 中创作,所有这些也都没有问题。

    • 正如noseratio 指出的那样,扩展 Promises 的主要用例是包装支持中止或取消逻辑(FileReader、fetch 等)的(旧版)API。

    【讨论】:

    • 但是如果你不保持与Promise构造函数的兼容性,你将无法使用MyPromise.raceMyPromise.all,违反了LSP SOLID原则。对于Symbol.toStringTag,是的,没什么用,我添加它只是为了完整性
    • Symbol.species getter 返回Promise 会导致调用MyPromise 对象的then 方法返回一个Promise 对象而不是MyPromise 对象,这使得扩展最多是部分的。如果省略 Symbol.species getter,调用继承的 Mypromise 对象的 then 方法会引发错误,因为“扩展”类构造函数不支持执行器函数(如帖子中所述)。
    【解决方案4】:

    我发现延长承诺的最佳方式是

    class MyPromise extends Promise {
        constructor(name) {
            // needed for MyPromise.race/all ecc
            if(name instanceof Function){
                return super(name)
            }
            super((resolve, reject) => {
                setTimeout(() => {
                    resolve(1)
                }, 1000)
            })
    
            this.name = name
        }
    
        // you can also use Symbol.species in order to
        // return a Promise for then/catch/finally
        static get [Symbol.species]() {
            return Promise;
        }
    
        // Promise overrides his Symbol.toStringTag
        get [Symbol.toStringTag]() {
            return 'MyPromise';
        }
    }
    
    
    new MyPromise('p1')
        .then(result => {
            console.log('resolved, result: ', result)
        })
        .catch(err => {
            console.error('err: ', err)
        })
    

    【讨论】:

    猜你喜欢
    • 2019-11-19
    • 2023-03-26
    • 2019-08-06
    • 1970-01-01
    • 2013-07-02
    • 2013-06-22
    • 1970-01-01
    • 2015-10-09
    • 1970-01-01
    相关资源
    最近更新 更多