TypeScript "this" Scoping-Problem beim Aufruf in jquery callback

107

Ich bin mir nicht sicher, wie ich das Scoping von "this" in TypeScript am besten handhaben kann.

Hier ist ein Beispiel für ein allgemeines Muster in dem Code, den ich in TypeScript konvertiere:

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

Jetzt könnte ich den Anruf ändern in ...

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

... was funktioniert. Aber es ist irgendwie schrecklich. Dies bedeutet, dass Code unter bestimmten Umständen kompiliert werden kann und einwandfrei funktioniert. Wenn wir jedoch vergessen, den Bereich zu binden, wird er beschädigt.

Ich möchte eine Möglichkeit, dies innerhalb der Klasse zu tun, damit wir uns bei der Verwendung der Klasse keine Gedanken darüber machen müssen, worauf "dies" abzielt.

Irgendwelche Vorschläge?

Aktualisieren

Ein anderer Ansatz, der funktioniert, ist die Verwendung des Fettpfeils:

class DemonstrateScopingProblems {
    private status = "blah";

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

Ist das ein gültiger Ansatz?

Jonathan Moffatt
quelle
2
Dies wäre hilfreich: youtube.com/watch?v=tvocUcbCupA
Basarat
Hinweis: Ryan hat seine Antwort in das TypeScript-Wiki kopiert .
Franklin Yu
Suchen Sie hier nach einer TypeScript 2+ -Lösung.
Deilan

Antworten:

166

Sie haben hier einige Optionen, jede mit ihren eigenen Kompromissen. Leider gibt es keine offensichtlich beste Lösung und es wird wirklich von der Anwendung abhängen.

Automatische Klassenbindung
Wie in Ihrer Frage gezeigt:

class DemonstrateScopingProblems {
    private status = "blah";

    public run = () => {
        alert(this.status);
    }
}
  • Gut / Schlecht: Dies erzeugt einen zusätzlichen Abschluss pro Methode pro Instanz Ihrer Klasse. Wenn diese Methode normalerweise nur in regulären Methodenaufrufen verwendet wird, ist dies ein Overkill. Wenn es jedoch häufig in Rückrufpositionen verwendet wird, ist es für die Klasseninstanz effizienter, den thisKontext zu erfassen, anstatt dass jede Aufrufsite beim Aufrufen einen neuen Abschluss erstellt.
  • Gut: Für externe Anrufer ist es unmöglich, den thisKontext zu vergessen
  • Gut: Typesicher in TypeScript
  • Gut: Keine zusätzliche Arbeit, wenn die Funktion Parameter hat
  • Schlecht: Abgeleitete Klassen können keine auf diese Weise geschriebenen Basisklassenmethoden aufrufen super.
  • Schlecht: Die genaue Semantik, welche Methoden "vorgebunden" sind und welche keinen zusätzlichen nicht typsicheren Vertrag zwischen Ihrer Klasse und ihren Verbrauchern erstellen.

Function.bind
Auch wie gezeigt:

$(document).ready(thisTest.run.bind(thisTest));
  • Gut / schlecht: Gegenüberliegender Kompromiss zwischen Speicher und Leistung im Vergleich zur ersten Methode
  • Gut: Keine zusätzliche Arbeit, wenn die Funktion Parameter hat
  • Schlecht: In TypeScript hat dies derzeit keine Typensicherheit
  • Schlecht: Nur in ECMAScript 5 verfügbar, wenn Ihnen das wichtig ist
  • Schlecht: Sie müssen den Instanznamen zweimal eingeben

Fetter Pfeil
In TypeScript (hier mit einigen Dummy-Parametern aus erklärenden Gründen gezeigt):

$(document).ready((n, m) => thisTest.run(n, m));
  • Gut / schlecht: Gegenüberliegender Kompromiss zwischen Speicher und Leistung im Vergleich zur ersten Methode
  • Gut: In TypeScript hat dies 100% Typensicherheit
  • Gut: Funktioniert in ECMAScript 3
  • Gut: Sie müssen den Instanznamen nur einmal eingeben
  • Schlecht: Sie müssen die Parameter zweimal eingeben
  • Schlecht: Funktioniert nicht mit variadischen Parametern
Ryan Cavanaugh
quelle
1
+1 Tolle Antwort Ryan, ich liebe die Aufschlüsselung der Vor- und Nachteile, danke!
Jonathan Moffatt
- In Ihrer Function.bind erstellen Sie jedes Mal einen neuen Abschluss, wenn Sie das Ereignis anhängen müssen.
131
1
Der dicke Pfeil hat es einfach gemacht !! : D: D = () => Vielen Dank! : D
Christopher Stock
@ Ryan-Cavanaugh Was ist mit dem Guten und dem Schlechten in Bezug darauf, wann das Objekt befreit wird? Wie im Beispiel eines SPA, das länger als 30 Minuten aktiv ist, welches der oben genannten Verfahren eignet sich am besten für JS-Garbage Collectors?
Abbaf33f
All dies wäre frei, wenn die Klasseninstanz frei ist. Die beiden letzteren können früher freigegeben werden, wenn die Lebensdauer des Ereignishandlers kürzer ist. Im Allgemeinen würde ich sagen, dass es keinen messbaren Unterschied geben wird.
Ryan Cavanaugh
16

Eine andere Lösung, die eine anfängliche Einrichtung erfordert, sich jedoch durch ihre unbesiegbar leichte, buchstäblich aus einem Wort bestehende Syntax auszahlt, ist die Verwendung von Method Decorators zum JIT-Binden von Methoden über Getter.

Ich habe auf GitHub ein Repo erstellt , um eine Implementierung dieser Idee zu präsentieren (es ist etwas langwierig, in eine Antwort mit 40 Codezeilen einschließlich Kommentaren zu passen) , die Sie so einfach verwenden würden wie:

class DemonstrateScopingProblems {
    private status = "blah";

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

Ich habe dies noch nirgendwo erwähnt, aber es funktioniert einwandfrei. Es gibt auch keinen nennenswerten Nachteil bei diesem Ansatz: Die Implementierung dieses Dekorators - einschließlich einiger Typprüfungen zur Laufzeit-Typensicherheit - ist trivial und unkompliziert und verursacht nach dem ersten Methodenaufruf im Wesentlichen keinen Overhead.

Der wesentliche Teil besteht darin, den folgenden Getter für den Klassenprototyp zu definieren, der unmittelbar vor dem ersten Aufruf ausgeführt wird:

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

Vollständige Quelle


Die Idee kann auch noch einen Schritt weiter gehen, indem Sie dies stattdessen in einem Klassendekorateur tun, Methoden durchlaufen und den obigen Eigenschaftsdeskriptor für jede von ihnen in einem Durchgang definieren.

John Weisz
quelle
genau das, was ich brauchte!
Marcel van der Drift
14

Nekromantie.
Es gibt eine offensichtliche einfache Lösung, die keine Pfeilfunktionen (Pfeilfunktionen sind 30% langsamer) oder JIT-Methoden durch Getter erfordert.
Diese Lösung besteht darin, den this-Kontext im Konstruktor zu binden.

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


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

Sie können eine Autobind-Methode schreiben, um alle Funktionen im Konstruktor der Klasse automatisch zu binden:

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

Beachten Sie, dass wenn Sie die Autobind-Funktion nicht in dieselbe Klasse wie eine Member-Funktion einfügen, dies gerecht ist autoBind(this);und nichtthis.autoBind(this);

Außerdem wird die obige AutoBind-Funktion heruntergefahren, um das Prinzip zu zeigen.
Wenn dies zuverlässig funktionieren soll, müssen Sie testen, ob die Funktion auch ein Getter / Setter einer Eigenschaft ist, da andernfalls - boom - Ihre Klasse Eigenschaften enthält.

So was:

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
quelle
Ich musste "autoBind (this)" verwenden, nicht "this.autoBind (this)"
JohnOpincar
@JohnOpincar: Ja, this.autoBind (this) geht davon aus, dass sich die automatische Bindung innerhalb der Klasse befindet und nicht als separater Export.
Stefan Steiger
Ich verstehe jetzt. Sie setzen die Methode auf dieselbe Klasse. Ich habe es in ein "Utility" -Modul eingefügt.
JohnOpincar
2

Haben Sie in Ihrem Code versucht, die letzte Zeile wie folgt zu ändern?

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