在jQuery回调中调用TypeScript“this”范围问题

Posted

技术标签:

【中文标题】在jQuery回调中调用TypeScript“this”范围问题【英文标题】:TypeScript "this" scoping issue when called in jquery callback 【发布时间】:2014-01-04 19:13:32 【问题描述】:

我不确定在 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);
    

这是一种有效的方法吗?

【问题讨论】:

这会很有帮助:youtube.com/watch?v=tvocUcbCupA 注意:Ryan 将他的答案复制到 TypeScript Wiki。 查看 here 以获取 TypeScript 2+ 解决方案。 【参考方案1】:

这里有几个选项,每个选项都有自己的取舍。不幸的是,没有明显的最佳解决方案,它实际上取决于应用程序。

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

class DemonstrateScopingProblems 
    private status = "blah";

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

好/坏:这会为类的每个实例的每个方法创建一个额外的闭包。如果这个方法通常只用在常规的方法调用中,那就大材小用了。但是,如果它在回调位置大量使用,则类实例捕获 this 上下文会更有效,而不是每个调用站点在调用时创建一个新的闭包。 好:外部调用者不可能忘记处理 this 上下文 好:TypeScript 中的类型安全 很好:如果函数有参数,则无需额外工作 错误:派生类无法调用使用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 很好的回答瑞恩,喜欢利弊的细分,谢谢! - 在 Function.bind 中,每次需要附加事件时都会创建一个新的闭包。 胖箭头刚刚做到了!! :D :D =()=> 非常感谢! :D @ryan-cavanaugh 关于对象何时被释放的好与坏呢?就像一个 SPA 的例子中超过 30 分钟一样,上面哪一个最适合 JS 垃圾收集器处理? 当类实例是可释放的时,所有这些都将是可释放的。如果事件处理程序的生命周期较短,则后两者将更早释放。不过,总的来说,我会说不会有可衡量的差异。【参考方案2】:

另一种解决方案需要一些初始设置,但由于其无敌的轻量级、字面意思是一个单词的语法而获得回报,是使用 Method Decorators 通过 getter 来 JIT-bind 方法。

我创建了一个 repo on GitHub 来展示这个想法的实现(用它的 40 行代码,包括 cmets 来适应答案有点冗长),你会使用就像:

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];

Full source


这个想法还可以更进一步,通过在类装饰器中执行此操作,迭代方法并一次性为每个方法定义上述属性描述符。

【讨论】:

正是我所需要的!【参考方案3】:

死灵术。 有一个明显简单的解决方案,不需要箭头函数(箭头函数慢 30%)或通过 getter 的 JIT 方法。 该解决方案是在构造函数中绑定 this-context。

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-function 与成员函数放在同一个类中,则它只是 autoBind(this); 而不是 this.autoBind(this);

另外,为了说明原理,上面的 autoBind 函数被简化了。 如果您希望它可靠地工作,您需要测试该函数是否也是属性的 getter/setter,否则 - 繁荣 - 如果您的类包含属性,那就是。

像这样:

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)
            
                if (!desc.configurable) 
                    console.log("AUTOBIND-WARNING: Property \"" + key + "\" not configurable ! (" + self.constructor.name + ")");
                    continue;
                

                let g = desc.get != null;
                let s = desc.set != null;

                if (g || s)
                
                    var newGetter = null;
                    var newSetter = null;
  
                    if (g)
                        newGetter = desc.get.bind(self);

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

                    if (newGetter != null && newSetter == null) 
                        Object.defineProperty(self, key, 
                            get: newGetter,
                            enumerable: desc.enumerable,
                            configurable: desc.configurable
                        );
                    
                    else if (newSetter != null && newGetter == null) 
                        Object.defineProperty(self, key, 
                            set: newSetter,
                            enumerable: desc.enumerable,
                            configurable: desc.configurable
                        );
                    
                    else 
                        Object.defineProperty(self, key, 
                            get: newGetter,
                            set: newSetter,
                            enumerable: desc.enumerable,
                            configurable: desc.configurable
                        );
                    
                    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:是的,this.autoBind(this) 假设 autobind 在类内部,而不是作为单独的导出。 我现在明白了。你把方法放在同一个类上。我把它放到一个“实用程序”模块中。【参考方案4】:

在您的代码中,您是否尝试过如下更改最后一行?

$(document).ready(() => thisTest.run());

【讨论】:

以上是关于在jQuery回调中调用TypeScript“this”范围问题的主要内容,如果未能解决你的问题,请参考以下文章

Typescript:如何使用回调调用 Javascript 函数

TypeScript 回调没有在其签名中完全实例化类

TypeScript 中的 JQuery POST 请求

带有 Typescript 错误的玩笑:超时 - 在 jest.setTimeout.Timeout 指定的 5000 毫秒超时内未调用异步回调

删除行时调用的 JQuery 数据表回调

从 servlet 中调用错误 jQuery ajax 回调