'this'/Backbone 遗留应用程序中的范围问题,_.bind 无法解决

'this'/scope problem in Backbone legacy app, that _.bind isn't solving

对于一个已有 7 年历史的 Backbone 网站,我有一个视图“PaymentInfoForm”,我需要在其中实现一个新的小部件来处理在线支付。新的小部件(我们称之为 inceptionLib)基本上是第三方 UI 和第四方 API 的验证包装器。 inceptionLib 是全新的、专有的,并且没有很好的文档记录,所以它是一个相当不透明的黑盒子,但我可以让它在独立演示中正常工作 JavaScript。但是,尝试与 Backbone 集成给我带来了麻烦。

inceptionLib 要求我创建一个新函数 inceptionLibFieldListener 作为我的 PaymentInfoForm 视图的一部分,以在用户与小部件交互时处理状态变化。在该函数中,我需要做的是能够设置我的视图的几个属性。 'this' 关键字有问题。我知道 'this' 在内部函数中使用时会失去我的视图范围,但我不确定为什么会发生在这里(除非它与黑匣子内的某些东西有关!)。如果我试图将我的函数包装在 _.bind 中作为此类问题的常见解决方案,'this' 只会成为对 window 对象的引用——就好像它“跳过了一个级别”在我的对象模型中向上。我不知道如何强制 'this' 成为我的视图——就像下一个函数一样——这样我就可以从这里修改我的视图属性。

感觉 inceptionLib 正在劫持 'this' 而且,好吧,我真的需要它回来!任何帮助表示赞赏。

App.PaymentInfoForm = Backbone.View.extend({ // existing view

    oldProperty1: null, // existing property
    myNewProperty2: null, // new property that I need to modify from inceptionLibFieldListener()

    initialize: function (){...}, // create objects & stuff, doesn't matter

    render: function(){ // render, including putting the inceptionLib widget on the page
        {...}
        var inceptionLibForm = inceptionLib.createForm();
        var that = this;
        vgsForm.initInceptionLib().then(function(){
            inceptionLibForm.renderCreditCard('#cc-container', that.inceptionLibFieldListener); 
            inceptionLibForm.renderExpirationDate('#exp-container', that.inceptionLibFieldListener);
        })
        return this;
    },  

    inceptionLibFieldListener: function(newState, prevState, flags) { // inceptionLib listener function
        console.log(this); // result is output for the inceptionLib object
    },
    
    // if I change the inceptionLibFieldListener to this:
    inceptionLibFieldListener: _.bind(function(newState, prevState, flags) { // _.bind should fix it
        console.log(this); // but result is Window object
    // }, this),
    
    somethingElse: function(){ // unrelated function just to test the difference in 'this' 
        console.log(this); // result is console output for the view, as expected
    },
    
    ...

您的问题来自对 this 工作原理的误解。 this我就不详细解释了,因为MDN上有一篇good, comprehensive article。但是,我可以简短地解释为什么您的代码不起作用以及如何修复它。

当你做的时候

var that = this;
vgsForm.initInceptionLib().then(function(){
    inceptionLibForm.renderCreditCard('#cc-container', that.inceptionLibFieldListener); 
    inceptionLibForm.renderExpirationDate('#exp-container', that.inceptionLibFieldListener);
})

that 确保 inceptionLibFieldListener 是从 PaymentInfoForm 实例中获得的,这是您试图实现的目标的一半。但是,执行 x.aMethod 不会 aMethod 绑定到 x。换句话说,当 aMethod 运行时, this 仍然可以是任何东西。以下代码演示了这一点:

var x = {
    aMethod: function() {
        console.log(this.aProperty);
    },
    aProperty: 1
};

var y = {
    aProperty: 2
};

var aMethod = x.aMethod; // correct function but not bound

aMethod(); // prints undefined
aMethod.call(y); // prints 2
x.aMethod(); // prints 1

您可能想知道为什么 aMethod 确实 似乎绑定到最后一个示例中的 x。为什么 x.aMethod() 有效但 aMethod = x.aMethod; aMethod() 无效?为什么你不需要总是调用 aMethod.call(x)?这是因为 JavaScript 引擎将 x.aMethod(a, b, c) 识别为特殊情况并自动将其转换为 x.aMethod.call(x, a, b, c)。你可能会觉得这有悖常理,而且你说得有道理,但这正是 JS 的工作方式。

当你做的时候

inceptionLibFieldListener: _.bind(function(newState, prevState, flags) { // _.bind should fix it
    console.log(this); // but result is Window object
}, this)

问题是 this 是在任何函数上下文之外计算的。 this 在全局范围内总是 window 对象,或者在 ES6 模块范围内是 undefined。所以 _.bind 正在做它应该做的事情,但你传递的是错误的 this.

您可以采取两种措施来解决此问题。第一个选项是在更合适的时候使用 _.bind,当 this 确实具有您需要的值时,例如在您的 render 方法中:

var boundInceptionFieldListener = _.bind(this.inceptionLibFieldListener, this);
vgsForm.initInceptionLib().then(function(){
    inceptionLibForm.renderCreditCard('#cc-container', boundInceptionFieldListener); 
    inceptionLibForm.renderExpirationDate('#exp-container', boundInceptionFieldListener);
})

另一种选择是使用 _.bindAll,它会永久更改方法以始终绑定到您的 PaymentInfoForm 实例。这是您可以在 contructorinitialize 方法中执行的操作,但如果您可能还想以未绑定形式使用该方法,我不推荐这样做:

_.bindAll(this, 'inceptionLibFieldListener');

使用后一种方法,您现有的 render 代码应该按原样工作。