在jQuery回调中调用时TypeScript“此”作用域问题


107

我不确定在TypeScript中处理“ this”作用域的最佳方法。

这是我转换为TypeScript的代码中常见模式的示例:

class DemonstrateScopingProblems {
    private status = "blah";
    public run() {
        alert(this.status);
    }
}

var thisTest = new DemonstrateScopingProblems();
// works as expected, displays "blah":
thisTest.run(); 
// doesn't work; this is scoped to be the document so this.status is undefined:
$(document).ready(thisTest.run); 

现在,我可以将呼叫更改为...

$(document).ready(thisTest.run.bind(thisTest));

...有效 但这有点可怕。这意味着在某些情况下,所有代码都可以编译并且可以正常工作,但是如果我们忘记绑定范围,它将被破坏。

我想要一种在类中执行此操作的方法,这样在使用类时,我们无需担心“ this”的作用域。

有什么建议?

更新资料

另一种可行的方法是使用粗箭头:

class DemonstrateScopingProblems {
    private status = "blah";

    public run = () => {
        alert(this.status);
    }
}

那是有效的方法吗?


2
这会有所帮助:youtube.com/watch?
v=tvocUcbCupA

注意:Ryan将他的答案复制到TypeScript Wiki
富兰克林于

在此处查找TypeScript 2+解决方案。
Deilan '17

Answers:


166

您在这里有一些选择,每个选择都有其自身的取舍。不幸的是,没有明显的最佳解决方案,它实际上取决于应用程序。

自动类绑定
如您的问题所示:

class DemonstrateScopingProblems {
    private status = "blah";

    public run = () => {
        alert(this.status);
    }
}
  • 好/不好:这将为您的类的每个实例的每个方法创建一个额外的闭包。如果通常只在常规方法调用中使用此方法,那就太过分了。但是,如果在回调位置上使用了大量的代码,则对于类实例而言,捕获this上下文而不是每个调用站点在调用时创建一个新的闭包更为有效。
  • 良好:外部呼叫者无法忘记处理this上下文
  • 良好:TypeScript中的Typesafe
  • 很好:如果函数具有参数,则无需额外的工作
  • 坏:派生类无法调用以这种方式编写的基类方法 super.
  • 坏:哪些方法是“预绑定”的,哪些没有在您的类及其使用者之间创建额外的非类型安全协定的确切语义。

Function.bind
也如下所示:

$(document).ready(thisTest.run.bind(thisTest));
  • 好/坏:与第一种方法相比,内存/性能处于相反的平衡
  • 很好:如果函数具有参数,则无需额外的工作
  • 坏:在TypeScript中,当前没有类型安全性
  • 错误:仅对您有影响,仅在ECMAScript 5中可用
  • 不好:您必须两次输入实例名称

粗体箭头
在TypeScript中(出于说明原因,此处显示了一些虚拟参数):

$(document).ready((n, m) => thisTest.run(n, m));
  • 好/坏:与第一种方法相比,内存/性能处于相反的平衡
  • 良好:在TypeScript中,这具有100%的类型安全性
  • 良好:可在ECMAScript 3中使用
  • 好:您只需键入一次实例名称
  • 坏:您必须输入两次参数
  • 坏:不适用于可变参数

1
+1很好的答案瑞安(Ryan),喜欢优缺点的分解,谢谢!
乔纳森·莫法特

-每次需要附加事件时,在Function.bind中创建一个新的闭包。
2015年

1
胖箭头就做到了!:D:D =()=>非常感谢!:D
Christopher Stock

@ ryan-cavanaugh关于何时释放对象的好坏呢?就像SPA处于活动状态> 30分钟的示例一样,以上哪种最适合JS垃圾收集器处理?
abbaf33f

当类实例是可释放的时,所有这些都将是可释放的。如果事件处理程序的生存期较短,则后两个将更早释放。总的来说,我会说这没有可测量的差异。
Ryan Cavanaugh

16

另一个需要一些初始设置但以其无敌的轻量级回报的解决方案是字面上的单字语法,即使用方法装饰器通过getter进行JIT绑定方法。

我已经在GitHub上创建了一个仓库来展示这个想法的实现(要用40行代码(包括注释)来回答这个问题有点冗长),您可以这样简单地使用:

class DemonstrateScopingProblems {
    private status = "blah";

    @bound public run() {
        alert(this.status);
    }
}

我还没有在任何地方看到此内容,但是它可以完美地工作。同样,这种方法也没有明显的缺点:这种装饰器的实现(包括对运行时类型安全性进行一些类型检查)是简单而直接的,并且在初始方法调用之后的开销基本上为零。

基本部分是在类原型上定义以下getter,该方法将在首次调用之前立即执行:

get: function () {
    // Create bound override on object instance. This will hide the original method on the prototype, and instead yield a bound version from the
    // instance itself. The original method will no longer be accessible. Inside a getter, 'this' will refer to the instance.
    var instance = this;

    Object.defineProperty(instance, propKey.toString(), {
        value: function () {
            // This is effectively a lightweight bind() that skips many (here unnecessary) checks found in native implementations.
            return originalMethod.apply(instance, arguments);
        }
    });

    // The first invocation (per instance) will return the bound method from here. Subsequent calls will never reach this point, due to the way
    // JavaScript runtimes look up properties on objects; the bound method, defined on the instance, will effectively hide it.
    return instance[propKey];
}

全文


通过在类装饰器中执行此操作,而不是遍历方法并一次通过为每个方法定义上述属性描述符,也可以使该方法更进一步。


正是我所需要的!
Marcel van der Drift'Mar

14

死灵法师。
有一个显而易见的简单解决方案,它不需要箭头功能(箭头功能慢30%)或通过吸气剂的JIT方法。
该解决方案是在构造函数中绑定此上下文。

class DemonstrateScopingProblems 
{
    constructor()
    {
        this.run = this.run.bind(this);
    }


    private status = "blah";
    public run() {
        alert(this.status);
    }
}

您可以编写一个autobind方法来自动绑定该类的构造函数中的所有函数:

class DemonstrateScopingProblems 
{

    constructor()
    { 
        this.autoBind(this);
    }
    [...]
}


export function autoBind(self)
{
    for (const key of Object.getOwnPropertyNames(self.constructor.prototype))
    {
        const val = self[key];

        if (key !== 'constructor' && typeof val === 'function')
        {
            // console.log(key);
            self[key] = val.bind(self);
        } // End if (key !== 'constructor' && typeof val === 'function') 

    } // Next key 

    return self;
} // End Function autoBind

需要注意的是,如果你不把autobind功能在同一类的成员函数,它只是autoBind(this);this.autoBind(this);

而且,上面的autoBind函数被简化,以显示原理。
如果希望此方法可靠地工作,则还需要测试该函数是否也是属性的获取器/设置器,否则-繁荣-如果您的类包含属性。

像这样:

export function autoBind(self)
{
    for (const key of Object.getOwnPropertyNames(self.constructor.prototype))
    {

        if (key !== 'constructor')
        {
            // console.log(key);

            let desc = Object.getOwnPropertyDescriptor(self.constructor.prototype, key);

            if (desc != null)
            {
                let g = desc.get != null;
                let s = desc.set != null;

                if (g || s)
                {
                    if (g)
                        desc.get = desc.get.bind(self);

                    if (s)
                        desc.set = desc.set.bind(self);

                    Object.defineProperty(self.constructor.prototype, key, desc);
                    continue; // if it's a property, it can't be a function 
                } // End if (g || s) 

            } // End if (desc != null) 

            if (typeof (self[key]) === 'function')
            {
                let val = self[key];
                self[key] = val.bind(self);
            } // End if (typeof (self[key]) === 'function') 

        } // End if (key !== 'constructor') 

    } // Next key 

    return self;
} // End Function autoBind

我必须使用“ autoBind(this)”而不是“ this.autoBind(this)”
JohnOpincar,

@JohnOpincar:是的,this.autoBind(this)假定autobind在类内部,而不是作为单独的导出。
Stefan Steiger,

我现在知道了。您将方法放在相同的类上。我将其放入“实用程序”模块中。
JohnOpincar '18

2

在您的代码中,您是否尝试过仅按以下方式更改最后一行?

$(document).ready(() => thisTest.run());
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.