web-dev-qa-db-fra.com

Comment préserver la portée lexicale dans TypeScript avec une fonction de rappel

J'ai une classe TypeScript, avec une fonction que je compte utiliser comme callback:

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

Je le passe à une autre fonction

this.deleteRow(this.removeRow);

qui appelle à son tour une méthode jQuery Ajax qui, en cas de succès, appelle le rappel de la manière suivante:

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 rappel, comme indiqué ci-dessus. Cela fonctionne, mais c'est un code de pantalon. Si je ne câble pas le 'this' comme ceci (désolé), alors toute référence à ceci dans la méthode de rappel est retournée à l'objet Window. Parce que j'utilise des fonctions fléchées jusqu'au bout, je m'attendais à ce que ce soit la classe elle-même, comme c'est le cas ailleurs dans ma classe.

Quelqu'un sait comment passer des rappels dans TypeScript, en préservant la portée lexicale?

36
Ralph Lavelle

Modifier le 28/01/2014:

Nouveaux lecteurs, assurez-vous de consulter La réponse de Zac ci-dessous .

Il a une solution beaucoup plus astucieuse qui vous permettra de définir et d’instancier une fonction étendue dans la définition de classe en utilisant la syntaxe fat arrow.

La seule chose que je voudrais ajouter est que, en ce qui concerne 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 répétition à l'aide de cette syntaxe:

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

Edit 2013-05-28:

La syntaxe permettant de définir un type de propriété de fonction a été modifiée (à partir de TypeScript version 0.8).

Auparavant, vous définiriez un type de fonction comme ceci:

class Test {
   removeRow: (): void;
}

Cela a maintenant changé pour:

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

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

En outre: Si vous devez définir plusieurs signatures de fonction pour le même nom de fonction (surcharge de la fonction d'exécution, par exemple), vous pouvez utiliser la notation de mappe d'objet (utilisée fréquemment dans le fichier descripteur jQuery):

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

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

Vous pouvez le faire de différentes manières.

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 voulez minimiser votre constructeur, vous pouvez simplement conserver la méthode removeRow dans la définition de la classe et simplement affecter 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

Et enfin, si vous utilisez une bibliothèque telle que underscore ou jQuery, vous pouvez simplement utiliser leur méthode d'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.
    }

}

Ensuite, vous pouvez ranger un peu votre méthode 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!");
    });
}
56
Sly_cardinal

UPDATE: Voir la réponse mise à jour de Sly. Il intègre une version améliorée des options ci-dessous.

AUTRE MISE À JOUR: génériques

Parfois, vous souhaitez spécifier un type générique dans une signature de fonction sans avoir à le spécifier sur la classe entière. Il m'a fallu quelques essais pour comprendre la syntaxe, alors j'ai pensé qu'il pourrait être intéressant de le partager:

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 conservent la déclaration de fonction et l'implémentation 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 (){

      }

}

26
Zac Morris

C'est en quelque sorte un message croisé tiré d'une autre réponse ( Existe-t-il un alias pour 'this' dans TypeScript? ). J'ai ré-appliqué le concept en utilisant les exemples ci-dessus. Je l’aime mieux que les options ci-dessus, car il prend explicitement en charge "la" portée à la fois de l’instance de la classe et de l’entité de contexte dynamique qui appelle la méthode.

Il existe deux versions ci-dessous. J'aime le premier parce que le compilateur aide à l'utiliser correctement (vous n'essaierez pas aussi facilement de mal utiliser le rappel lambda lui-même que le rappel, en raison 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();
1
Eric

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

Exemple de code de travail:

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

Le code en question d'origine deviendrait quelque chose comme ceci:

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

Il va définir le contexte (this) à l'intérieur de la fonction de rappel sur tout ce qui a été passé en tant qu'argument pour lier une fonction, dans ce cas, cet objet d'origine.

1
Dmitriy

S'appuyant sur les réponses sournoises et de Zac avec les types suivants: Un exemple complet de bonjour le monde. J'espère que cela est le bienvenu, vu que c'est le premier résultat de Google lorsque vous recherchez "rappels javascript TypeScript"

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 transpilé en:

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 trouve cela un peu utile. :)

0
DarkNeuron