Problem z określaniem zakresu „this” w języku TypeScript w przypadku wywołania zwrotnego jQuery

107

Nie jestem pewien najlepszego podejścia do obsługi określania zakresu „this” w języku TypeScript.

Oto przykład typowego wzorca w kodzie, który konwertuję na 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); 

Teraz mogę zmienić połączenie na ...

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

... który działa. Ale to trochę straszne. Oznacza to, że cały kod może się skompilować i działać dobrze w pewnych okolicznościach, ale jeśli zapomnimy powiązać zakres, to się zepsuje.

Chciałbym znaleźć sposób na zrobienie tego w ramach klasy, aby podczas korzystania z klasy nie musieli się martwić o to, do czego odnosi się „to”.

Jakieś sugestie?

Aktualizacja

Innym podejściem, które działa, jest użycie grubej strzałki:

class DemonstrateScopingProblems {
    private status = "blah";

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

Czy to prawidłowe podejście?

Jonathan Moffatt
źródło
2
To byłoby pomocne: youtube.com/watch?v=tvocUcbCupA
basarat
Uwaga: Ryan skopiował swoją odpowiedź do TypeScript Wiki .
Franklin Yu
Spójrz tutaj do sporządzania roztworu maszynopis 2+.
Deilan

Odpowiedzi:

166

Masz tutaj kilka opcji, z których każda ma swoje własne kompromisy. Niestety nie ma oczywistego najlepszego rozwiązania i będzie to naprawdę zależało od aplikacji.

Automatyczne wiązanie klas
Jak pokazano w Twoim pytaniu:

class DemonstrateScopingProblems {
    private status = "blah";

    public run = () => {
        alert(this.status);
    }
}
  • Dobra / zła: tworzy dodatkowe zamknięcie dla każdej metody na instancję Twojej klasy. Jeśli ta metoda jest zwykle używana tylko w zwykłych wywołaniach metod, jest to przesada. Jeśli jednak jest często używany na pozycjach wywołania zwrotnego, bardziej wydajne jest, aby instancja klasy przechwytywała thiskontekst, zamiast tworzenia przez każdą witrynę wywołania nowego zamknięcia po wywołaniu.
  • Dobrze: rozmówcy zewnętrzni nie mogą zapomnieć o obsłudze thiskontekstu
  • Dobrze: Typesafe w języku TypeScript
  • Dobrze: brak dodatkowej pracy, jeśli funkcja ma parametry
  • Źle: klasy pochodne nie mogą wywoływać metod klasy bazowej napisanych w ten sposób przy użyciu super.
  • Źle: dokładna semantyka, które metody są „wstępnie powiązane”, a które nie tworzą dodatkowego kontraktu, który nie jest bezpieczny dla typu, między klasą a jej użytkownikami.

Function.bind
Również jak pokazano:

$(document).ready(thisTest.run.bind(thisTest));
  • Dobra / zła: przeciwny kompromis między pamięcią a wydajnością w porównaniu z pierwszą metodą
  • Dobrze: brak dodatkowej pracy, jeśli funkcja ma parametry
  • Źle: w TypeScript nie ma obecnie żadnego zabezpieczenia typów
  • Źle: Dostępne tylko w ECMAScript 5, jeśli ma to dla Ciebie znaczenie
  • Źle: musisz dwukrotnie wpisać nazwę instancji

Strzałka tłuszczu w języku
TypeScript (pokazana tutaj z paroma fikcyjnymi parametrami z powodów wyjaśniających):

$(document).ready((n, m) => thisTest.run(n, m));
  • Dobra / zła: przeciwny kompromis między pamięcią a wydajnością w porównaniu z pierwszą metodą
  • Dobrze: w TypeScript ma to 100% bezpieczeństwo typów
  • Dobrze: działa w ECMAScript 3
  • Dobrze: wystarczy wpisać nazwę instancji tylko raz
  • Źle: musisz dwukrotnie wpisać parametry
  • Źle: Nie działa z parametrami zmiennymi
Ryan Cavanaugh
źródło
1
+1 Świetna odpowiedź Ryan, uwielbiam zestawienie zalet i wad, dzięki!
Jonathan Moffatt
- W swoim Function.bind tworzysz nowe zamknięcie za każdym razem, gdy musisz dołączyć zdarzenie.
131
1
Gruba strzała właśnie to zrobiła !! : D: D = () => Dziękuję bardzo! : D
Christopher Stock
@ ryan-cavanaugh a co z tym, co dobre i złe, kiedy obiekt zostanie uwolniony? Jak w przykładzie SPA, które jest aktywne przez> 30 minut, które z powyższych jest najlepsze dla śmieciarek JS?
abbaf33f
Wszystko to byłoby wolne, gdyby instancja klasy była wolna. Te dwa ostatnie będą dostępne wcześniej, jeśli czas życia programu obsługi zdarzeń jest krótszy. Generalnie powiedziałbym, że nie będzie żadnej mierzalnej różnicy.
Ryan Cavanaugh
16

Innym rozwiązaniem, które wymaga pewnej wstępnej konfiguracji, ale opłaca się swoją niezwyciężoną lekką, dosłownie jednowyrazową składnią, jest użycie dekoratorów metod do wiązania metod JIT przez gettery.

Utworzyłem repozytorium na GitHub, aby zaprezentować implementację tego pomysłu (dopasowanie do odpowiedzi z 40 wierszami kodu, w tym komentarzami , jest trochę za długie) , którego możesz użyć tak prosto, jak:

class DemonstrateScopingProblems {
    private status = "blah";

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

Nigdzie nie widziałem tego wspomnianego, ale działa bezbłędnie. Nie ma również zauważalnej wady tego podejścia: implementacja tego dekoratora - w tym pewne sprawdzanie typu pod kątem bezpieczeństwa typów w czasie wykonywania - jest trywialna i prosta, a po początkowym wywołaniu metody generuje praktycznie zerowy narzut.

Istotną częścią jest zdefiniowanie następującego gettera na prototypie klasy, który jest wykonywany bezpośrednio przed pierwszym wywołaniem:

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

Pełne źródło


Pomysł można również pójść o krok dalej, robiąc to w dekoratorze klas, iterując po metodach i definiując powyższy deskryptor właściwości dla każdej z nich w jednym przebiegu.

John Weisz
źródło
dokładnie to, czego potrzebowałem!
Marcel van der Drift
14

Nekromancja.
Istnieje oczywiste proste rozwiązanie, które nie wymaga funkcji strzałkowych (funkcje strzałkowe są o 30% wolniejsze) ani metod JIT poprzez metody pobierające.
Rozwiązaniem jest powiązanie tego kontekstu w konstruktorze.

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


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

Możesz napisać metodę autobind, która będzie automatycznie wiązała wszystkie funkcje w konstruktorze klasy:

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

Zwróć uwagę, że jeśli nie umieścisz funkcji autobind w tej samej klasie co funkcja składowa, to po prostu autoBind(this);niethis.autoBind(this);

Ponadto powyższa funkcja autoBind jest przytłumiona, aby pokazać zasadę.
Jeśli chcesz, aby to działało niezawodnie, musisz sprawdzić, czy funkcja jest również pobierającą / ustawiającą właściwości, ponieważ w przeciwnym razie - boom - jeśli twoja klasa zawiera właściwości, to znaczy.

Lubię to:

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
źródło
Musiałem użyć „autoBind (this)”, a nie „this.autoBind (this)”
JohnOpincar
@JohnOpincar: tak, this.autoBind (this) zakłada, że ​​autobind znajduje się wewnątrz klasy, a nie jako oddzielny eksport.
Stefan Steiger
Teraz rozumiem. Umieszczasz metodę w tej samej klasie. Umieściłem go w module „narzędziowym”.
JohnOpincar
2

Czy w swoim kodzie próbowałeś po prostu zmienić ostatnią linię w następujący sposób?

$(document).ready(() => thisTest.run());
Albino Cordeiro
źródło