如何使用回调函数在 TypeScript 中保留词法范围

IT技术 javascript callback typescript
2021-03-04 03:04:17

我有一个 TypeScript 类,有一个我打算用作回调的函数:

removeRow(_this:MyClass): void {
    ...
    // 'this' is now the window object
    // I must use '_this' to get the class itself
    ...
}

我将它传递给另一个函数

this.deleteRow(this.removeRow);

它反过来调用一个 jQuery Ajax 方法,如果成功,它会像这样调用回调:

deleteItem(removeRowCallback: (_this:MyClass) => void ): void {
    $.ajax(action, {
        data: { "id": id },
        type: "POST"
    })
    .done(() => {
        removeRowCallback(this);
    })
    .fail(() => {
        alert("There was an error!");
    });
}

我可以保留对我的类的“this”引用的唯一方法是将其传递给回调,如上所示。它有效,但它是裤子代码。如果我没有像这样连接“this”(抱歉),那么回调方法中对 this 的任何引用都已恢复到 Window 对象。因为我一直在使用箭头函数,所以我预计“this”将是类本身,因为它在我的类中的其他地方。

任何人都知道如何在 TypeScript 中传递回调,保留词法范围?

5个回答

编辑 2014-01-28:

新读者,请务必查看下方扎克的回答

他有一个更简洁的解决方案,可以让您使用粗箭头语法在类定义中定义和实例化作用域函数。

我要补充的唯一一件事是,关于Zac 回答中的选项 5,可以使用以下语法指定方法签名和返回类型而无需任何重复:

public myMethod = (prop1: number): string => {
    return 'asdf';
}

编辑 2013-05-28:

定义函数属性类型的语法已更改(自 TypeScript 0.8 版起)。

以前,您会像这样定义函数类型:

class Test {
   removeRow: (): void;
}

现在已更改为:

class Test {
   removeRow: () => void;
}

我已经更新了下面的答案以包含此新更改。

另外:如果您需要为相同的函数名称定义多个函数签名(例如运行时函数重载),那么您可以使用对象映射表示法(这在 jQuery 描述符文件中被广泛使用):

class Test {
    removeRow: {
        (): void;
        (param: string): string;
    };
}

您需要将签名定义为removeRow()类的属性,但在构造函数中分配实现。

有几种不同的方法可以做到这一点。

选项1

class Test {

    // Define the method signature here.
    removeRow: () => void;

    constructor (){
        // Implement the method using the fat arrow syntax.
        this.removeRow = () => {
            // Perform your logic to remove the row.
            // Reference `this` as needed.
        }
    }

}

如果你想让你的构造函数最小化,那么你可以removeRow在类定义中保留方法,并在构造函数中分配一个代理函数:

选项 2

class Test {

    // Again, define the method signature here.
    removeRowProxy: () => void;

    constructor (){
        // Assign the method implementation here.
        this.removeRowProxy = () => {
            this.removeRow.apply(this, arguments);
        }
    }

    removeRow(): void {
        // ... removeRow logic here.
    }

}

选项 3

最后,如果您使用像 underscore 或 jQuery 这样的库,那么您可以使用它们的实用程序方法来创建代理:

class Test {

    // Define the method signature here.
    removeRowProxy: () => void;

    constructor (){
        // Use jQuery to bind removeRow to this instance.
        this.removeRowProxy = $.proxy(this.removeRow, this);
    }

    removeRow(): void {
        // ... removeRow logic here.
    }

}

然后你可以deleteItem稍微整理一下你的方法:

// Specify `Function` as the callback type.
// NOTE: You can define a specific signature if needed.
deleteItem(removeRowCallback: Function ): void {
    $.ajax(action, {
        data: { "id": id },
        type: "POST"
    })

    // Pass the callback here.
    // 
    // You don't need the fat arrow syntax here
    // because the callback has already been bound
    // to the correct scope.
    .done(removeRowCallback)

    .fail(() => {
        alert("There was an error!");
    });
}
嗯,感谢全面的回答!我试过没有。2,它工作得很好。但是你有一个错字:应该只是“{”,而不是“{}”。
2021-04-18 03:04:17
@AlexDresko 他们无法真正更改此功能,因为这与 JavaScript 管理prototype函数范围的方式有关例如Greeter.prototype.greet,在此处查看函数如何在编译脚本中结束:typescriptlang.org/Playground当函数被定义时,没有要绑定到的实例。这就是为什么我们必须在这种情况下解决它 - 有时这是一件好事,有时是一件坏事,但至少它是已知的:)
2021-04-18 03:04:17
嗯,主要是为了解决这个问题,我们必须跳过语法障碍。如果 TS 提供一些语言糖使其更易于处理,我想我会很高兴。:)
2021-04-21 03:04:17
我真的希望他们能在 TypeScript 中改变这个功能,所以这总是在类的上下文中......如果有必要,也许有一些其他的方法来获取调用者的上下文。但是,话又说回来,我通常不知道我在说什么。
2021-04-27 03:04:17
oop,解决了这个问题。我认为自动完成太有用了:P
2021-05-12 03:04:17

更新:请参阅 Sly 的更新答案。它包含以下选项的改进版本。

另一个更新:泛型

有时您想在函数签名中指定一个泛型类型,而不必在整个类中指定它。我花了几次尝试弄清楚语法,所以我认为它可能值得分享:

class MyClass {  //no type parameter necessary here

     public myGenericMethod = <T>(someArg:string): QPromise<T> => {

         //implementation here...

     }
}

选项 4

这里有几个语法可以添加到 Sly_cardinal 的答案中。这些示例将函数声明和实现保持在同一位置:

class Test {

      // Define the method signature AND IMPLEMENTATION here.
      public removeRow: () => void = () => {

        // Perform your logic to remove the row.
        // Reference `this` as needed.

      }

      constructor (){

      }

}

或者

选项 5

更紧凑一点,但放弃显式返回类型(如果不是显式,编译器应该推断返回类型):

class Test {

      // Define implementation with implicit signature and correct lexical scope.
      public removeRow = () => {

        // Perform your logic to remove the row.
        // Reference `this` as needed.

      }

      constructor (){

      }

}

杰出的!我没有意识到您可以在方法定义上使用粗箭头语法。我认为它会错误地在所有类实例中共享相同的函数实例 - 但看起来 TypeScript 比这更聪明:)
2021-04-24 03:04:17
请注意,不能使用 super 访问使用粗箭头语法定义的方法,这可能使它们成为面向对象继承的主要痛苦。有关处理此问题的方法,请参阅该问题。
2021-04-30 03:04:17
既然您已经发布了更好(更正确)的解决方案,我已经更新了我的答案以指向您的答案。我还添加了关于选项 5 的注释 - 我们可以指定参数和返回类型而无需任何重复:)
2021-05-01 03:04:17
感谢 5 的提示!我试过但无法找出正确的语法来获取返回类型。我讨厌让事情隐含。
2021-05-03 03:04:17
谢谢你。只是一个小纸条:它不具备成为构造以上(它是一个变量好的,但在我的情况下,它开始为这是给另一个对象的方法,它调用其他一些保护方法-所以,重要的我将其置于构造函数下方)
2021-05-12 03:04:17

使用 .bind() 在回调中保留上下文。

工作代码示例:

window.addEventListener(
  "resize",
  (()=>{this.retrieveDimensionsFromElement();}).bind(this)
)

原始问题中的代码将变成这样:

$.ajax(action, {
    data: { "id": id },
    type: "POST"
})
.done(
  (() => {
    removeRowCallback();
  }).bind(this)
)

它将回调函数内的上下文 (this) 设置为作为参数传递给绑定函数的任何内容,在本例中为原始 this 对象。

简单直接!
2021-05-02 03:04:17

这是来自另一个答案的交叉帖子(TypeScript 中是否有“this”的别名?)。我使用上面的例子重新应用了这个概念。我比上面的选项更喜欢它,因为它明确支持类实例以及调用该方法的动态上下文实体的“this”范围。

下面有两个版本。我喜欢第一个,因为在正确使用它(你将不容易试图滥用回调lambda本身编译助攻回调,因为显式类型参数)。

测试一下:http : //www.typescriptlang.org/Playground/

class Test {

    private testString: string = "Fancy this!";

    // Define the method signature here.
    removeRowLambdaCallback(outerThis: Test): {(): void} {
        alert("Defining callback for consumption");
        return function(){
            alert(outerThis.testString); // lexically scoped class instance
            alert(this); // dynamically scoped context caller
            // Put logic here for removing rows. Can refer to class
            // instance as well as "this" passed by a library such as JQuery or D3.
        }
    }
    // This approach looks nicer, but is more dangerous
    // because someone might use this method itself, rather
    // than the return value, as a callback.
     anotherRemoveRowLambdaCallback(): {(): void} {
        var outerThis = this;
        alert("Defining another callback for consumption");
        return function(){
            alert(outerThis.testString); // lexically scoped class instance
            alert(this); // dynamically scoped context caller
            // Put logic here for removing rows. Can refer to class
            // instance as well as "this" passed by a library such as JQuery or D3.
        }
    }
}

var t = new Test();
var callback1 = t.removeRowLambdaCallback(t);
var callback2 = t.anotherRemoveRowLambdaCallback();

callback1();
callback2();
关于必须传递实例方法的事情,实例本身让我想洗澡......前两个答案更清晰。编译器_this为词法范围自动创建,并且this仍然引用动态上下文。不太确定通过您的解决方案获得的优势。
2021-05-09 03:04:17

基于 sly 和 Zac 对类型的回答:一个完整​​的 hello world 示例。我希望这是受欢迎的,因为这是谷歌搜索“typescript javascript callbacks”时的最佳结果

type MyCallback = () => string;

class HelloWorld {

    // The callback
    public callback: MyCallback = () => {
        return 'world';
    }

    // The caller
    public caller(callback: MyCallback) {
        alert('Hello ' + callback());
    }
}

let hello = new HelloWorld();
hello.caller(hello.callback);

这被转译为:

var HelloWorld = (function () {
    function HelloWorld() {
        // The callback
        this.callback = function () {
            return 'world';
        };
    }
    // The caller
    HelloWorld.prototype.caller = function (callback) {
        alert('Hello ' + callback());
    };
    return HelloWorld;
}());
var hello = new HelloWorld();
hello.caller(hello.callback);

希望有人觉得它有点用。:)