web-dev-qa-db-fra.com

TypeScript "ce" problème de portée lorsqu'il est appelé dans un rappel jquery

Je ne suis pas sûr de la meilleure approche pour gérer la portée de "ceci" dans TypeScript.

Voici un exemple de motif commun dans le code que je convertis en 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); 

Maintenant, je pourrais changer l'appel en ...

$(document).ready(thisTest.run.bind(thisTest));

... qui fonctionne. Mais c'est un peu horrible. Cela signifie que le code peut tout compiler et fonctionner correctement dans certaines circonstances, mais si nous oublions de lier la portée, il se cassera.

Je voudrais un moyen de le faire au sein de la classe, de sorte que, lors de l'utilisation de la classe, nous n'ayons pas à nous soucier de ce à quoi "ceci" est destiné.

Aucune suggestion?

Mise à jour

Une autre approche qui fonctionne consiste à utiliser la grosse flèche:

class DemonstrateScopingProblems {
    private status = "blah";

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

Est-ce une approche valide?

104
Jonathan Moffatt

Vous avez quelques options ici, chacune avec ses propres compromis. Malheureusement, il n'y a pas de solution évidente et dépendra vraiment de l'application.

Liaison de classe automatique
Comme le montre votre question:

class DemonstrateScopingProblems {
    private status = "blah";

    public run = () => {
        alert(this.status);
    }
}
  • Bon/mauvais: Cela crée une fermeture supplémentaire par méthode et par instance de votre classe. Si cette méthode est généralement utilisée uniquement dans des appels de méthode standard, il s'agit d'une surcharge. Cependant, s'il est beaucoup utilisé dans les positions de rappel, il est plus efficace pour l'instance de classe de capturer le contexte this au lieu de chaque site d'appel créant une nouvelle fermeture à l'appel.
  • Bon: Impossible pour les appelants externes d'oublier de gérer le contexte this
  • Bon: Typesafe dans TypeScript
  • Bon: pas de travail supplémentaire si la fonction a des paramètres
  • Mauvais: les classes dérivées ne peuvent pas appeler les méthodes de classe de base écrites de cette façon en utilisant super.
  • Bad: La sémantique exacte des méthodes "prédéfinies" et des méthodes qui ne sont pas créées crée un contrat supplémentaire non typifère entre votre classe et ses consommateurs.

Function.bind
Également comme indiqué:

$(document).ready(thisTest.run.bind(thisTest));
  • Bon/mauvais: compromis mémoire/performances opposé par rapport à la première méthode
  • Bon: pas de travail supplémentaire si la fonction a des paramètres
  • Mauvais: dans TypeScript, ceci n'a actuellement aucun type de sécurité
  • Mauvais: disponible uniquement dans ECMAScript 5, si cela vous importe
  • Bad: vous devez taper le nom de l'instance deux fois

grosse flèche
Sous TypeScript (indiqué ici avec quelques paramètres factices pour des raisons explicatives):

$(document).ready((n, m) => thisTest.run(n, m));
  • Bon/mauvais: compromis mémoire/performances opposé par rapport à la première méthode
  • Bon: Dans TypeScript, cela a 100% de sécurité de type
  • Bon: Fonctionne dans ECMAScript 3
  • Bon: il suffit de saisir le nom de l'instance une seule fois
  • Mauvais: vous devrez taper les paramètres deux fois
  • Bad: Ne fonctionne pas avec les paramètres variadiques
159
Ryan Cavanaugh

Une autre solution nécessitant une configuration initiale, mais payante avec sa légèreté invinciblement légère, littéralement en un mot, utilise Method Decorators pour lier les méthodes JIT à l'aide de getters.

J'ai créé un repo sur GitHub pour présenter une implémentation de cette idée (il est un peu long de s'adapter à une réponse avec ses 40 lignes de code, commentaires inclus ) , que vous utiliseriez aussi simplement:

class DemonstrateScopingProblems {
    private status = "blah";

    @bound public run() {
        alert(this.status);
    }
}

Je n'ai encore jamais vu cela mentionné, mais cela fonctionne parfaitement. En outre, cette approche ne présente pas d'inconvénient notable: la mise en œuvre de ce décorateur - , y compris une vérification de type pour la sécurité du type à l'exécution - est triviale. et simple, et vient avec essentiellement zéro overhead après l'appel de méthode initiale.

La partie essentielle consiste à définir le getter suivant sur le prototype de classe, qui est exécuté immédiatement avant le premier appel:

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

source complète


L'idée peut également être poussée un peu plus loin, en effectuant cette opération dans un décorateur de classe, en effectuant une itération sur des méthodes et en définissant le descripteur de propriété ci-dessus pour chacune d'elles en une seule passe.

14
John Weisz

Nécromancie.
Il existe une solution simple, évidente, qui n'exige pas de fonctions fléchées (les fonctions fléchées sont 30% plus lentes), ni de méthodes JIT via des accesseurs.
Cette solution consiste à lier le this-context dans le constructeur.

class DemonstrateScopingProblems 
{
    constructor()
    {
        this.run = this.run.bind(this);
    }


    private status = "blah";
    public run() {
        alert(this.status);
    }
}

Vous pouvez utiliser cette méthode pour lier automatiquement toutes les fonctions de la classe dans le constructeur:

class DemonstrateScopingProblems 
{

    constructor()
    { 
        this.autoBind(this);
    }
    [...]
}


export function autoBind(self: any)
{
    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
14
Stefan Steiger

Dans votre code, avez-vous essayé de changer la dernière ligne comme suit?

$(document).ready(() => thisTest.run());
2
Albino Cordeiro