在javascript中使用promise和原型时,优雅的回调绑定

Elegant callback binding when using promises and prototypes in javascript

本文关键字:绑定 回调 原型 javascript promise      更新时间:2023-09-26

我是一个重度javascript原型和promise用户。

我的问题是,每当我履行承诺时,我都需要使用.bind(this)来设置正确的上下文。

以下是显示问题的示例代码(jsbin):

var Q = require('Q');
var AsyncCounter = function(initialValue){
  this.value = initialValue;
};
AsyncCounter.prototype.increment=function(){
  return Q.fcall(function(){
    return ++this.value;
  }.bind(this));
};
AsyncCounter.prototype.decrement=function(){
  return Q.fcall(function(){
    return --this.value;
  }.bind(this));
};
var counter = new AsyncCounter(10);
counter.increment()
  .then(function(incrementedValue){
    console.log('incremented value:', incrementedValue)
  })
  .then(counter.decrement.bind(counter))
  .then(function(decrementedValue){
    console.log('decremented value:', decrementedValue)
  });

看看我需要多久依赖一次bind()?我觉得这太不合法了。

我确实知道petkaantonov/bluebird promise库及其在回调上传播作用域的非常有用的bind(scope)函数,但我觉得有一种更好的原生方法可以做到这一点。

有人有合适的方法吗?

还记得当您学习了Foo.prototype.bar=function(){...}并放弃将构造函数内的方法定义为this.bar = function(){...}时吗?原因是速度和内存的效率。

现在你有充分的理由让时间倒流,牺牲速度/效率来实现"可分离"的方法,即特定于具有this就绪绑定的构造函数的实例的方法,这正是你想要的。

如果多次调用这些方法,那么在引用函数时不必使用bind(),将大大弥补构造的低效性。此外,你还可以获得你所寻求的句法上的便利。

以下是您的示例的一个版本,该版本经过了重新调整,以更好地展示语法上的便利性:

var AsyncCounter = function(value){
    this.increment = function(){
      return Q.fcall(function(){
        return console.log("++value: " + ++value);
      }.bind(this));
    }.bind(this);
    this.decrement = function(){
      return Q.fcall(function(){
        return console.log("--value: " + --value);
      }.bind(this));
    }.bind(this);
};
var counter = new AsyncCounter(10);
counter.increment()
  .then(counter.decrement)
  .then(counter.increment);

总之,您需要做两件事:

  • 在构造函数内定义方法
  • 将CCD_ 8与方法结合

我认为另一种方法是利用闭包。promise中使用的每个回调都可以包含对创建它的根作用域的一些变量的引用。

通常,大多数人所做的是创建一个名为self的变量,该变量主要引用外部this。因此,无论您在哪里执行promise的函数,闭包都将维护引用,从而能够访问范围。

AsyncCounter.prototype.increment=function(){
   var self = this;
   return Q.fcall(function(){
    return ++self .value;
  });
};

老实说,我不确定哪一个更优雅。我不确定是否有其他方法可以在不使用此技术或bind方法的情况下完成您想要的操作。

在我看来,最好的解决方案是添加一个context方法来承诺在未来的then调用中为函数调用指定this。这似乎是我在一些库中看到的,不记得在哪里,并且在我自己的库中实现的。这个想法是写

promise.context(this).then(myfunc)

我不知道Q,所以我不知道扩展它/自定义它以实现这种行为是否容易,甚至可能。以下是这个想法的玩具实现,使用原生JS promise:

Promise.prototype.context = function (ctxt) { this.ctxt = ctxt; return this; }
Promise.prototype.then = (function() {
    var old_then = Promise.prototype.then;
    return function(fulfill, reject) {
        return old_then.call(this,
            fulfill && fulfill.bind && fulfill.bind(this.ctxt),
            reject  && reject.bind  && reject.bind(this.ctxt)
        );
    };
}());

实际上,您可以经常编写代码,这样就不会出现context调用,只需在创建promise的地方执行一次即可。因此:

var MyTrip = {
    gotoMars: function() { return InterPlanetaryTrip(Mars).context(this); },
    report:   function() { console.log("I'm back, Mom!"); },
    go:       function() { gotoMars.then(this.report).then(this.drink); }
};

这种方法省去了我无限的烦恼,而且我看不到任何负面影响。无论这意味着什么,我都看不到比这更"本土"的方式了。

侵入性较小的方法

作为对你的问题的更直接的回答,你可以引入一点糖——一种一次性绑定并调用Q.fcall的方法,如下所示:

AsyncCounter.prototype.fcall = function(fn) { 
    return Q.fcall(fn.bind(this)); 
};
AsyncCounter.prototype.increment = function() { 
    return this.fcall(function() {
        return ++this.value;
    });
};

如果使用ES6,可以使用箭头函数和词法范围。

AsyncCounter.prototype.increment = () => {
    return Q.fcall(() => ++this.value);
};