Problème de portée de TypeScript "this" lorsqu'il est appelé dans le rappel jquery

107

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

Voici un exemple d'un modèle 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));

... ce 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 dans la classe, de sorte que lors de l'utilisation de la classe, nous n'ayons pas à nous soucier de la portée de «ceci».

Aucune suggestion?

Mettre à 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 valable?

Jonathan Moffatt
la source
2
Ce serait utile: youtube.com/watch?v=tvocUcbCupA
basarat
Remarque: Ryan a copié sa réponse dans le Wiki TypeScript .
Franklin Yu
Recherchez ici une solution TypeScript 2+.
Deilan

Réponses:

166

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

Liaison de classe automatique
Comme indiqué dans 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 n'est généralement utilisée que dans les appels de méthode réguliers, c'est excessif. Cependant, s'il est beaucoup utilisé dans les positions de rappel, il est plus efficace pour l'instance de classe de capturer le thiscontexte au lieu que chaque site d'appel crée une nouvelle fermeture lors de l'appel.
  • Bon: impossible pour les appelants externes d'oublier de gérer le thiscontexte
  • 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.
  • Mauvais: la sémantique exacte des méthodes "pré-liées" et de celles qui ne créent pas un contrat non sécurisé supplémentaire entre votre classe et ses consommateurs.

Function.bind
Aussi 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, il n'y a actuellement aucune sécurité de type
  • Mauvais: uniquement disponible dans ECMAScript 5, si cela vous intéresse
  • Mauvais: vous devez taper le nom de l'instance deux fois

Grosse flèche
dans TypeScript (illustrée 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: vous ne devez taper qu'une seule fois le nom de l'instance
  • Mauvais: vous devrez taper les paramètres deux fois
  • Mauvais: ne fonctionne pas avec les paramètres variadiques
Ryan Cavanaugh
la source
1
+1 Excellente réponse Ryan, j'adore la répartition des avantages et des inconvénients, merci!
Jonathan Moffatt
- Dans votre Function.bind, vous créez une nouvelle fermeture à chaque fois que vous devez joindre l'événement.
131
1
La grosse flèche vient de le faire !! : D: D = () => Merci beaucoup! : D
Christopher Stock
@ ryan-cavanaugh qu'en est-il du bon et du mauvais en ce qui concerne le moment où l'objet sera libéré? Comme dans l'exemple d'un SPA actif pendant plus de 30 minutes, lequel des éléments ci-dessus est le mieux à gérer pour les garbage collector JS?
abbaf33f
Tous ces éléments seraient libérables lorsque l'instance de classe est libérable. Les deux derniers seront libérables plus tôt si la durée de vie du gestionnaire d'événements est plus courte. En général, je dirais qu'il n'y aura pas de différence mesurable, cependant.
Ryan Cavanaugh le
16

Une autre solution qui nécessite une configuration initiale mais qui porte ses fruits avec sa syntaxe invinciblement légère, littéralement à un mot, consiste à utiliser des décorateurs de méthode pour lier des méthodes JIT via des getters.

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

class DemonstrateScopingProblems {
    private status = "blah";

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

Je n'ai encore vu cela mentionné nulle part, mais cela fonctionne parfaitement. De plus, il n'y a pas d'inconvénient notable à cette approche: l'implémentation 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 n'entraîne pratiquement aucune surcharge après l'appel de la méthode initiale.

La partie essentielle est de 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 faisant cela dans un décorateur de classe à la place, en parcourant les méthodes et en définissant le descripteur de propriété ci-dessus pour chacune d'elles en une seule passe.

John Weisz
la source
juste ce dont j'avais besoin!
Marcel van der Drift
14

Nécromancie.
Il existe une solution simple et évidente qui ne nécessite pas de fonctions fléchées (les fonctions fléchées sont 30% plus lentes), ni de méthodes JIT via des getters.
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 écrire une méthode autobind pour lier automatiquement toutes les fonctions dans le constructeur de la classe:

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

Notez que si vous ne mettez pas la fonction autobind-function dans la même classe qu'une fonction membre, c'est juste autoBind(this);et nonthis.autoBind(this);

Et aussi, la fonction autoBind ci-dessus est simplifiée, pour montrer le principe.
Si vous voulez que cela fonctionne de manière fiable, vous devez tester si la fonction est également un getter / setter d'une propriété, car sinon - boom - si votre classe contient des propriétés, c'est.

Comme ça:

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)
            {
                let g = desc.get != null;
                let s = desc.set != null;

                if (g || s)
                {
                    if (g)
                        desc.get = desc.get.bind(self);

                    if (s)
                        desc.set = desc.set.bind(self);

                    Object.defineProperty(self.constructor.prototype, key, desc);
                    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
Stefan Steiger
la source
J'ai dû utiliser "autoBind (this)" et non "this.autoBind (this)"
JohnOpincar
@JohnOpincar: oui, this.autoBind (this) suppose que la liaison automatique est à l'intérieur de la classe, pas comme une exportation séparée.
Stefan Steiger
Je comprends maintenant. Vous mettez la méthode sur la même classe. Je l'ai mis dans un module "utilitaire".
JohnOpincar
2

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

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