在 jquery 回调中调用时的 TypeScript“this”范围问题

TypeScript "this" scoping issue when called in jquery callback

提问人:Jonathan Moffatt 提问时间:12/17/2013 最后编辑:CommunityJonathan Moffatt 更新时间:10/20/2021 访问量:60892

问:

我不确定在 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赞 basarat 2/13/2015
这将很有帮助:youtube.com/watch?v=tvocUcbCupA
0赞 Franklin Yu 12/21/2016
注意:Ryan 将他的答案复制到了 TypeScript Wiki
0赞 Deilan 9/19/2017
在此处查找 TypeScript 2+ 解决方案。

答:

175赞 Ryan Cavanaugh 12/17/2013 #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赞 Jonathan Moffatt 12/18/2013
+1 很棒的答案 Ryan,喜欢利弊的细分,谢谢!
0赞 131 4/22/2015
- 在 Function.bind 中,每次需要附加事件时都会创建一个新的闭包。
1赞 Christopher Stock 9/8/2016
胖箭就做到了!:D :D =()=> 非常感谢!:D
0赞 jmbmage 9/12/2016
@ryan-卡瓦诺,就物体何时被释放而言,好与坏呢?就像一个活动了 > 30 分钟的 SPA 的例子一样,以上哪一项最适合 JS 垃圾收集器处理?
0赞 Ryan Cavanaugh 9/12/2016
当类实例是可释放的时,所有这些都是可释放的。如果事件处理程序的生存期较短,则后两者将更早释放。不过,总的来说,我会说不会有可衡量的差异。
18赞 John Weisz 10/7/2016 #2

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

我在 GitHub 上创建了一个 repo 来展示这个想法的实现(用它的 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];
}

完整来源


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

评论

0赞 Marcel van der Drift 3/20/2017
正是我需要的!
2赞 Albino Cordeiro 3/24/2017 #3

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

$(document).ready(() => thisTest.run());
14赞 Stefan Steiger 9/18/2017 #4

死灵术。
有一个明显的简单解决方案,它不需要箭头函数(箭头函数慢 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 放入与成员函数相同的类中,则它是 just 而不是autoBind(this);this.autoBind(this);

而且,上面的 autoBind 函数被简化了,以显示原理。
如果你想让它可靠地工作,你需要测试该函数是否也是属性的 getter/setter,因为否则 - boom - 如果你的类包含属性,那就是。

喜欢这个:

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

评论

0赞 JohnOpincar 5/30/2018
我不得不使用“autoBind(this)”而不是“this.autoBind(this)”
0赞 Stefan Steiger 5/30/2018
@JohnOpincar:是的,this.autoBind(this) 假设 autobind 在类内部,而不是作为单独的导出。
0赞 JohnOpincar 5/30/2018
我现在明白了。将该方法放在同一个类上。我把它放在一个“实用”模块中。