42 votes

Comment préserver la portée lexicale en TypeScript avec une fonction callback ?

J'ai une classe TypeScript, avec une fonction que j'ai l'intention d'utiliser comme rappel :

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

Je le transmets à une autre fonction

this.deleteRow(this.removeRow);

qui appelle à son tour une méthode Ajax de jQuery, qui, en cas de succès, invoque le rappel comme suit :

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

La seule façon de préserver la référence "this" à ma classe est de la transmettre au callback, comme démontré ci-dessus. Cela fonctionne, mais c'est du code pantalon. Si je ne connecte pas le "this" comme ceci (désolé), alors toute référence à "this" dans la méthode de rappel est retournée à l'objet Window. Parce que j'utilise des fonctions fléchées, je m'attendais à ce que le "this" soit la classe elle-même, comme c'est le cas ailleurs dans ma classe.

Quelqu'un sait-il comment passer des callbacks en TypeScript en préservant la portée lexicale ?

61voto

Sly_cardinal Points 3109

Editer 2014-01-28 :

Nouveaux lecteurs, ne manquez pas de consulter Réponse de Zac ci-dessous .

Il propose une solution beaucoup plus soignée qui vous permet de définir et d'instancier une fonction dans la définition de la classe à l'aide de la syntaxe fat arrow.

La seule chose que j'ajouterai est que, en ce qui concerne les option 5 dans la réponse de Zac, il est possible de spécifier la signature de la méthode et le type de retour sans aucune répétition en utilisant cette syntaxe :

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

Editer 2013-05-28 :

La syntaxe pour définir un type de propriété de fonction a changé (depuis la version 0.8 de TypeScript).

Auparavant, vous définissiez un type de fonction comme suit :

class Test {
   removeRow: (): void;
}

Il s'agit maintenant d'un changement :

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

J'ai mis à jour ma réponse ci-dessous pour inclure ce nouveau changement.

Par ailleurs : Si vous devez définir plusieurs signatures de fonctions pour le même nom de fonction (par exemple, la surcharge de fonctions en cours d'exécution), vous pouvez utiliser la notation de carte d'objet (cette notation est largement utilisée dans le fichier descripteur de jQuery) :

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

Vous devez définir la signature de removeRow() en tant que propriété de votre classe, mais assignez l'implémentation dans le constructeur.

Il y a plusieurs façons de procéder.

Option 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.
        }
    }

}

Si vous souhaitez que votre constructeur soit minimal, vous pouvez simplement conserver l'élément removeRow dans la définition de la classe et assigner simplement une fonction proxy dans le constructeur :

Option 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.
    }

}

Option 3

Enfin, si vous utilisez une bibliothèque comme underscore ou jQuery, vous pouvez utiliser leur méthode utilitaire pour créer le proxy :

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.
    }

}

Vous pouvez alors mettre de l'ordre dans votre deleteItem un peu :

// 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!");
    });
}

36voto

Zac Points 568

MISE À JOUR : voir la réponse actualisée de Sly. Elle comprend une version améliorée des options ci-dessous.

NOUVELLE MISE À JOUR : les génériques

Il arrive que l'on veuille spécifier un type générique dans la signature d'une fonction sans avoir à le spécifier dans toute la classe. Il m'a fallu quelques essais pour comprendre la syntaxe, et j'ai donc pensé que cela valait la peine d'être partagé :

class MyClass {  //no type parameter necessary here

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

         //implementation here...

     }
}

Option 4

Voici quelques syntaxes supplémentaires à ajouter à la réponse de Sly_cardinal. Ces exemples permettent de conserver la déclaration et l'implémentation de la fonction au même endroit :

class Test {

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

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

      }

      constructor (){

      }

}

ou

Option 5

Un peu plus compact, mais abandonne le type de retour explicite (le compilateur devrait déduire le type de retour de toute façon s'il n'est pas explicite) :

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 (){

      }

}

5voto

Dmitriy Points 114

Utilisez .bind() pour préserver le contexte dans le rappel.

Exemple de code de travail :

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

Le code de la question originale deviendrait quelque chose comme ceci :

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

Le contexte (this) à l'intérieur de la fonction de rappel sera remplacé par ce qui a été transmis en tant qu'argument à la fonction de liaison, dans ce cas l'objet original this.

2voto

Eric Points 120

Il s'agit en quelque sorte d'une réponse croisée à une autre réponse ( Existe-t-il un alias pour "this" en TypeScript ? ). J'ai réappliqué le concept en utilisant les exemples ci-dessus. Je la préfère aux options ci-dessus parce qu'elle prend explicitement en charge le cadrage "this" à la fois pour l'instance de la classe et pour l'entité de contexte dynamique qui appelle la méthode.

Il y a deux versions ci-dessous. J'aime la première parce que le compilateur aide à l'utiliser correctement (vous n'essayerez pas aussi facilement de mal utiliser la lambda callback elle-même). comme le callback, à cause du paramètre explicitement typé).

Testez-le : 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();

0voto

DarkNeuron Points 59

En s'appuyant sur les réponses de Sly et de Zac avec les types : Un exemple complet de hello world. J'espère que c'est bienvenu, vu que c'est le premier résultat dans Google, quand on cherche "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);

Ceci est transposé dans :

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

J'espère que quelqu'un le trouvera un peu utile :)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X