Gibt es in TypeScript einen Alias ​​für 'this'?

74

Ich habe versucht, eine Klasse in TypeScript zu schreiben, für die eine Methode definiert ist, die als Ereignishandler-Rückruf für ein jQuery-Ereignis fungiert.

class Editor {
    textarea: JQuery;

    constructor(public id: string) {
        this.textarea = $(id);
        this.textarea.focusin(onFocusIn);
    }

    onFocusIn(e: JQueryEventObject) {
        var height = this.textarea.css('height'); // <-- This is not good.
    }
}

In der Ereignisbehandlungsroutine onFocusIn sieht TypeScript 'this' als 'this' der Klasse. JQuery überschreibt diese Referenz jedoch und setzt sie auf das dem Ereignis zugeordnete DOM-Objekt.

Eine Alternative besteht darin, ein Lambda innerhalb des Konstruktors als Ereignishandler zu definieren. In diesem Fall erstellt TypeScript eine Art Abschluss mit einem versteckten _this-Alias.

class Editor {
    textarea: JQuery;

    constructor(public id: string) {
        this.textarea = $(id);
        this.textarea.focusin((e) => {
            var height = this.textarea.css('height'); // <-- This is good.
        });
    }
}

Meine Frage ist, gibt es eine andere Möglichkeit, mit TypeScript auf diese Referenz im methodenbasierten Ereignishandler zuzugreifen, um dieses jQuery-Verhalten zu überwinden?

Todd
quelle
3
Die Antwort mit () => {} sieht gut aus
Daniel Little

Antworten:

22

Wie bereits erwähnt, gibt es keinen TypeScript-Mechanismus, mit dem sichergestellt werden kann, dass eine Methode immer an ihren thisZeiger gebunden ist (und dies ist nicht nur ein jQuery-Problem). Dies bedeutet nicht, dass es keinen einigermaßen einfachen Weg gibt, dieses Problem zu beheben. Sie müssen einen Proxy für Ihre Methode generieren, der den thisZeiger wiederherstellt, bevor Sie Ihren Rückruf aufrufen. Sie müssen dann Ihren Rückruf mit diesem Proxy umschließen, bevor Sie ihn an das Ereignis übergeben. jQuery hat einen eingebauten Mechanismus dafür jQuery.proxy(). Hier ist ein Beispiel für Ihren obigen Code mit dieser Methode (beachten Sie den hinzugefügten $.proxy()Aufruf.)

class Editor { 
    textarea: JQuery; 

    constructor(public id: string) { 
        this.textarea = $(id); 
        this.textarea.focusin($.proxy(onFocusIn, this)); 
    } 

    onFocusIn(e: JQueryEventObject) { 
        var height = this.textarea.css('height'); // <-- This is not good. 
    } 
} 

Das ist eine vernünftige Lösung, aber ich persönlich habe festgestellt, dass Entwickler häufig vergessen, den Proxy-Aufruf einzuschließen, und habe daher eine alternative TypeScript-basierte Lösung für dieses Problem gefunden. Wenn Sie die HasCallbacksKlasse verwenden, müssen Sie Ihre Klasse nur von ableiten. Bei HasCallbacksallen Methoden, denen ein Präfix vorangestellt ist, 'cb_'wird der thisZeiger dauerhaft gebunden. Sie können diese Methode einfach nicht mit einem anderen thisZeiger aufrufen, was in den meisten Fällen vorzuziehen ist. Beide Mechanismen funktionieren so, dass sie einfach zu bedienen sind.

class HasCallbacks {
    constructor() {
        var _this = this, _constructor = (<any>this).constructor;
        if (!_constructor.__cb__) {
            _constructor.__cb__ = {};
            for (var m in this) {
                var fn = this[m];
                if (typeof fn === 'function' && m.indexOf('cb_') == 0) {
                    _constructor.__cb__[m] = fn;                    
                }
            }
        }
        for (var m in _constructor.__cb__) {
            (function (m, fn) {
                _this[m] = function () {
                    return fn.apply(_this, Array.prototype.slice.call(arguments));                      
                };
            })(m, _constructor.__cb__[m]);
        }
    }
}

class Foo extends HasCallbacks  {
    private label = 'test';

    constructor() {
        super();

    }

    public cb_Bar() {
        alert(this.label);
    }
}

var x = new Foo();
x.cb_Bar.call({});
Steven Ickman
quelle
1
Der $.proxy()Anruf ist eine großartige Lösung für meine Situation. Ein thisAlias ​​würde die TS komplizieren und, wie andere betont haben, nur schwer abwärtskompatibel sein.
Todd
1
Ich dachte, ich würde hinzufügen, dass ich weiß, dass der Code für HasCallbacks ein wenig beängstigend aussieht, aber alles, was Sie tun, ist, Ihre Klassenmitglieder zu durchlaufen und sie mit Proxys vorverdrahten. Wenn Ihr Projekt klein ist, ist es wahrscheinlich am besten, $ .proxy () zu verwenden. Wenn Ihr Projekt jedoch groß ist, führt die HasCallbacks-Klasse dazu, dass weniger Code auf den Client heruntergeladen wird (Sie haben nicht alle zusätzlichen $ .proxy () -Aufrufe) und weniger fehleranfällig ist. In Bezug auf die Ausführung haben beide Ansätze ungefähr die gleiche Leistung (HasCallbacks hat einen einmaligen Aufzählungsaufwand für jede Klasse), sodass Sie wirklich die Wahl haben.
Steven Ickman
3
Ich denke, Ihre Antwort kann irreführend sein, da ich den Eindruck hatte, dass TypeScript dies nicht unterstützt, während dies tatsächlich der Fall ist, wenn Sie Ihre Methoden nur mithilfe der Pfeilsyntax definieren.
Sam
Diese Antwort bietet möglicherweise eine Lösung, ist jedoch nicht korrekt, da TypeScript einen Mechanismus zur Aufrechterhaltung des lexikalischen Umfangs bietet. In Sams Antworten finden Sie zwei Möglichkeiten, wie Sie mit Pfeilfunktionen dieses Problem lösen können.
Bingles
99

Der Umfang von thisbleibt erhalten, wenn die Pfeilfunktionssyntax verwendet wird. () => { ... }Hier ist ein Beispiel aus TypeScript für JavaScript-Programmierer .

var ScopeExample = { 
  text: "Text from outer function", 
  run: function() { 
    setTimeout( () => { 
      alert(this.text); 
    }, 1000); 
  } 
};

Beachten Sie, dass this.textSie dadurch erhalten, Text from outer functiondass die Pfeilfunktionssyntax den "lexikalischen Bereich" beibehält.

Fenton
quelle
Der Kontext von "dies" wird mit Ihrem Beispiel nicht geändert, und daher gilt das, was Sie gezeigt haben, nicht wirklich.
Paul Mendoza
@PaulMendoza tatsächlich, wenn Sie es ändern, erhalten setTimeout( function () { alert(this.text); }, 1000);Sie undefined.
Fenton
Du hast recht. Mein Fehler. Irgendwie sagt das () => dem Compiler, dass dies etwas anderes bedeutet als function () {}
Paul Mendoza
9
Das ist wunderbar. Requisiten! Hinter den Kulissen wird ein var _this = thisoben in der Methode erstellt und anschließend _thisin der anonymen Funktion referenziert .
Richard Rout
21

Wie in einigen anderen Antworten beschrieben, führt die Verwendung der Pfeilsyntax zum Definieren einer Funktion dazu, dass Verweise thisimmer auf die einschließende Klasse verweisen.

Um Ihre Frage zu beantworten, finden Sie hier zwei einfache Problemumgehungen.

Verweisen Sie mit der Pfeilsyntax auf die Methode

constructor(public id: string) {
    this.textarea = $(id);
    this.textarea.focusin(e => this.onFocusIn(e));
}

Definieren Sie die Methode mithilfe der Pfeilsyntax

onFocusIn = (e: JQueryEventObject) => {
    var height = this.textarea.css('height');
}
Sam
quelle
6

Sie können eine Member-Funktion im Konstruktor an ihre Instanz binden.

class Editor {
    textarea: JQuery;

    constructor(public id: string) {
        this.textarea = $(id);
        this.textarea.focusin(onFocusIn);
        this.onFocusIn = this.onFocusIn.bind(this); // <-- Bind to 'this' forever
    }

    onFocusIn(e: JQueryEventObject) {
        var height = this.textarea.css('height');   // <-- This is now fine
    }
}

Alternativ binden Sie es einfach, wenn Sie den Handler hinzufügen.

        this.textarea.focusin(onFocusIn.bind(this));
Drew Noakes
quelle
Dies ist meine Lieblingsmethode, aber bindfür ältere Browser benötigen Sie leider eine Polyfüllung .
Antoine129
3

Versuche dies

class Editor 
{

    textarea: JQuery;
    constructor(public id: string) {
        this.textarea = $(id);
        this.textarea.focusin((e)=> { this.onFocusIn(e); });
    }

    onFocusIn(e: JQueryEventObject) {
        var height = this.textarea.css('height'); // <-- This will work
    }

}
Gabriel C.
quelle
3

Die Lösung von Steven Ickman ist praktisch, aber unvollständig. Die Antworten von Danny Becket und Sam sind kürzer und manueller und scheitern im gleichen allgemeinen Fall, wenn ein Rückruf vorliegt, der gleichzeitig dynamisch und lexikalisch "dies" erfordert. Springe zu meinem Code, wenn meine Erklärung unten TL ist; DR ...

Ich muss "this" für dynamisches Scoping für die Verwendung mit Bibliotheksrückrufen beibehalten, und ich muss ein "this" mit lexikalischem Scoping für die Klasseninstanz haben. Ich behaupte, dass es am elegantesten ist, die Instanz an einen Rückrufgenerator zu übergeben und den Parameter effektiv über die Klasseninstanz schließen zu lassen. Der Compiler teilt Ihnen mit, ob Sie dies verpasst haben. Ich verwende eine Konvention zum Aufrufen des Parameters mit lexikalischem Gültigkeitsbereich "OuterThis", aber "self" oder ein anderer Name könnte besser sein.

Die Verwendung des Schlüsselworts "this" wird aus der OO-Welt gestohlen, und als TypeScript es übernahm (aus den ECMAScript 6-Spezifikationen, nehme ich an), haben sie ein Konzept mit lexikalischem Gültigkeitsbereich und ein Konzept mit dynamischem Gültigkeitsbereich zusammengeführt, wenn eine Methode von einer anderen Entität aufgerufen wird . Ich bin ein wenig verärgert darüber; Ich würde ein "self" -Schlüsselwort in TypeScript bevorzugen, damit ich die lexikalisch begrenzte Objektinstanz davon übergeben kann. Alternativ könnte JS neu definiert werden, um bei Bedarf einen expliziten "Aufrufer" -Parameter an erster Stelle zu benötigen (und somit alle Webseiten auf einen Schlag zu unterbrechen).

Hier ist meine Lösung (aus einer großen Klasse herausgeschnitten). Sehen Sie sich insbesondere die Art und Weise an, wie die Methoden aufgerufen werden, und insbesondere den Körper von "dragmoveLambda":

export class OntologyMappingOverview {

initGraph(){
...
// Using D3, have to provide a container of mouse-drag behavior functions
// to a force layout graph
this.nodeDragBehavior = d3.behavior.drag()
        .on("dragstart", this.dragstartLambda(this))
        .on("drag", this.dragmoveLambda(this))
        .on("dragend", this.dragendLambda(this));

...
}

dragmoveLambda(outerThis: OntologyMappingOverview): {(d: any, i: number): void} {
    console.log("redefine this for dragmove");

    return function(d, i){
        console.log("dragmove");
        d.px += d3.event.dx;
        d.py += d3.event.dy;
        d.x += d3.event.dx;
        d.y += d3.event.dy; 

        // Referring to "this" in dynamic scoping context
        d3.select(this).attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });

        outerThis.vis.selectAll("line")
            .filter(function(e, i){ return e.source == d || e.target == d; })
            .attr("x1", function(e) { return e.source.x; })
            .attr("y1", function(e) { return e.source.y; })
            .attr("x2", function(e) { return e.target.x; })
            .attr("y2", function(e) { return e.target.y; });

    }
}

dragging: boolean  =false;
// *Call* these callback Lambda methods rather than passing directly to the callback caller.
 dragstartLambda(outerThis: OntologyMappingOverview): {(d: any, i: number): void} {
        console.log("redefine this for dragstart");

        return function(d, i) {
            console.log("dragstart");
            outerThis.dragging = true;

            outerThis.forceLayout.stop();
        }
    }

dragendLambda(outerThis: OntologyMappingOverview): {(d: any, i: number): void}  {
        console.log("redefine this for dragend");

        return function(d, i) {
            console.log("dragend");
            outerThis.dragging = false;
            d.fixed = true;
        }
    }

}
Eric
quelle
Obwohl diese Implementierung etwas hässlich ist, ist sie eine großartige Möglichkeit, die Entführung von Bereichen zu übertreffen. +1
Carrie Kendall
2

TypeScript bietet keine zusätzliche Möglichkeit (über die regulären JavaScript-Mittel hinaus), um zur "echten" thisReferenz zurückzukehren, außer thisder in der Lambda-Syntax für fette Pfeile (die aus rückenkompatibler Sicht zulässig ist, da kein vorhandener JS-Code vorhanden ist) könnte einen =>Ausdruck verwenden).

Sie könnten einen Vorschlag auf der CodePlex-Site veröffentlichen, aber aus Sicht des Sprachdesigns kann hier wahrscheinlich nicht viel passieren, da jedes vernünftige Schlüsselwort, das der Compiler bereitstellen könnte, möglicherweise bereits von vorhandenem JavaScript-Code verwendet wird.

Ryan Cavanaugh
quelle
2

Sie können die js eval-Funktion verwenden: var realThis = eval('this');

Reptil
quelle
1

Ich habe ein ähnliches Problem gehabt. Ich denke, Sie können .each()in vielen Fällen thiseinen anderen Wert für spätere Ereignisse verwenden.

Der JavaScript-Weg:

$(':input').on('focus', function() {
  $(this).css('background-color', 'green');
}).on('blur', function() {
  $(this).css('background-color', 'transparent');
});

Der TypeScript-Weg:

$(':input').each((i, input) => {
  var $input = $(input);
  $input.on('focus', () => {
    $input.css('background-color', 'green');
  }).on('blur', () => {
    $input.css('background-color', 'transparent');
  });
});

Ich hoffe das hilft jemandem.

obrys
quelle
0

Sie können Ihre Referenz auf thisin einer anderen Variablen speichern . selfVielleicht können Sie auf diese Weise auf die Referenz zugreifen. Ich verwende kein Typoskript, aber das ist eine Methode, die mir in der Vergangenheit mit Vanille-Javascript erfolgreich war.

Sheridan Bulger
quelle
2
Das einzige Problem bei dieser Lösung besteht darin, dass die thisReferenz innerhalb der Rückrufmethode die thisReferenz des zugrunde liegenden Objekts maskiert , sodass keine Möglichkeit besteht, über die richtige Eigenschaft auf die neue Eigenschaft zuzugreifen this.
Todd
1
Dies ist zwar ein gültiger Ansatz, aber ich denke, es ist besser, die integrierte Funktionalität von TypeScript zu verwenden, um dies zu erreichen.
Sam
2
Todd: Genau deshalb haben Sie das richtige "dies" in einer Variablen gespeichert, bevor der neue Bereich (und damit das neue "dies") eingegeben wird. Dies ist ein ziemlich häufiges Javascript-Problem, nicht nur Typoskript (@Sam). Ich bin mir nicht sicher, warum die Abstimmung, aber es ist alles gut.
Sheridan Bulger
@SheridanBulger, die Abwertung erfolgt, weil dies eine TypeScript-Frage ist und TypeScript bereits einen eigenen Weg bietet, um dies zu erreichen. Ich denke, es ist im Allgemeinen eine gute Idee, Sprachfunktionen zu verwenden, anstatt sie selbst neu zu implementieren.
Sam
@SheridanBulger Downvoted ... dies ist keine TypeScript-Antwort.
Benjamin
0

Es gibt eine viel einfachere Lösung als alle oben genannten Antworten. Grundsätzlich greifen wir auf JavaScript zurück, indem wir die Schlüsselwortfunktion anstelle des Konstrukts '=>' verwenden, das das 'this' in die Klasse 'this' übersetzt.

class Editor {
    textarea: JQuery;

    constructor(public id: string) {
        var self = this;                      // <-- This is save the reference
        this.textarea = $(id);
        this.textarea.focusin(function() {   // <-- using  javascript function semantics here
             self.onFocusIn(this);          //  <-- 'this' is as same as in javascript
        });
    }

    onFocusIn(jqueryObject : JQuery) {
        var height = jqueryObject.css('height'); 
    }
}
Derek Liang
quelle