Problema di scoping "this" di TypeScript quando viene chiamato in callback jquery


107

Non sono sicuro dell'approccio migliore per la gestione dell'ambito di "questo" in TypeScript.

Ecco un esempio di un modello comune nel codice che sto convertendo in 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); 

Ora, potrei cambiare la chiamata a ...

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

... che funziona. Ma è piuttosto orribile. Significa che tutto il codice può essere compilato e funzionare correttamente in alcune circostanze, ma se dimentichiamo di associare l'ambito si interromperà.

Vorrei un modo per farlo all'interno della classe, in modo che quando si usa la classe non dobbiamo preoccuparci di cosa sia "questo".

Eventuali suggerimenti?

Aggiornare

Un altro approccio che funziona è usare la freccia grassa:

class DemonstrateScopingProblems {
    private status = "blah";

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

È un approccio valido?


2
Questo sarebbe utile: youtube.com/watch?v=tvocUcbCupA
basarat

Nota: Ryan ha copiato la sua risposta su TypeScript Wiki .
Franklin Yu

Cerca qui una soluzione TypeScript 2+.
Deilan

Risposte:


166

Hai alcune opzioni qui, ognuna con i suoi compromessi. Sfortunatamente non esiste una soluzione ovvia migliore e dipenderà davvero dall'applicazione.

Associazione automatica della classe
Come mostrato nella tua domanda:

class DemonstrateScopingProblems {
    private status = "blah";

    public run = () => {
        alert(this.status);
    }
}
  • Buono / cattivo: crea una chiusura aggiuntiva per metodo per istanza della tua classe. Se questo metodo viene solitamente utilizzato solo nelle normali chiamate di metodo, è eccessivo. Tuttavia, se viene utilizzato molto nelle posizioni di callback, è più efficiente per l'istanza della classe acquisire il thiscontesto invece di ogni sito di chiamata che crea una nuova chiusura al momento della chiamata.
  • Corretto: impossibile per i chiamanti esterni dimenticare di gestire il thiscontesto
  • Buono: Typesafe in TypeScript
  • Buono: nessun lavoro extra se la funzione ha parametri
  • Cattivo: le classi derivate non possono chiamare i metodi della classe base scritti in questo modo utilizzando super.
  • Cattivo: la semantica esatta di quali metodi sono "pre-vincolati" e quali non creano un contratto aggiuntivo non tipografico tra la tua classe e i suoi consumatori.

Function.bind
Anche come mostrato:

$(document).ready(thisTest.run.bind(thisTest));
  • Buono / cattivo: compromesso opposto tra memoria e prestazioni rispetto al primo metodo
  • Buono: nessun lavoro extra se la funzione ha parametri
  • Cattivo: in TypeScript, questo attualmente non ha l'indipendenza dai tipi
  • Cattivo: disponibile solo in ECMAScript 5, se questo è importante per te
  • Cattivo: è necessario digitare due volte il nome dell'istanza

Freccia
grossa in TypeScript (mostrata qui con alcuni parametri fittizi per motivi esplicativi):

$(document).ready((n, m) => thisTest.run(n, m));
  • Buono / cattivo: compromesso opposto tra memoria e prestazioni rispetto al primo metodo
  • Buono: in TypeScript, questo ha il 100% di sicurezza dei tipi
  • Buono: funziona in ECMAScript 3
  • Corretto: devi solo digitare il nome dell'istanza una volta
  • Cattivo: dovrai digitare i parametri due volte
  • Cattivo: non funziona con i parametri variadici

1
+1 Ottima risposta Ryan, adoro la ripartizione dei pro e dei contro, grazie!
Jonathan Moffatt

- Nel tuo Function.bind, crei una nuova chiusura ogni volta che devi allegare l'evento.
131

1
La freccia grassa l'ha appena fatto !! : D: D = () => Grazie mille! : D
Christopher Stock,

@ ryan-cavanaugh che dire del bene e del male in termini di quando l'oggetto verrà liberato? Come nell'esempio di una SPA attiva per> 30 minuti, quale dei precedenti è il migliore da gestire per i garbage collector di JS?
abbaf33f

Tutti questi sarebbero liberabili quando l'istanza della classe è liberabile. Gli ultimi due saranno liberabili prima se la durata del gestore di eventi è più breve. In generale, però, direi che non ci sarà una differenza misurabile.
Ryan Cavanaugh,

16

Un'altra soluzione che richiede un po 'di configurazione iniziale ma ripaga con la sua sintassi invincibilmente leggera, letteralmente composta da una sola parola, è l'utilizzo di Method Decorators per legare JIT ai metodi tramite getter.

Ho creato un repository su GitHub per mostrare un'implementazione di questa idea (è un po 'lungo da inserire in una risposta con le sue 40 righe di codice, inclusi i commenti) , che useresti semplicemente come:

class DemonstrateScopingProblems {
    private status = "blah";

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

Non l'ho ancora visto menzionato da nessuna parte, ma funziona perfettamente. Inoltre, non vi è alcun aspetto negativo di rilievo in questo approccio: l'implementazione di questo decoratore, incluso un controllo del tipo per la sicurezza dei tipi in fase di esecuzione , è banale e semplice e viene fornito essenzialmente con zero overhead dopo la chiamata iniziale del metodo.

La parte essenziale è definire il seguente getter sul prototipo della classe, che viene eseguito immediatamente prima della prima chiamata:

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

Fonte completa


L'idea può anche essere spinta un passo avanti, eseguendo invece questo in un decoratore di classi, iterando sui metodi e definendo il descrittore di proprietà sopra per ciascuno di essi in un unico passaggio.


proprio quello di cui avevo bisogno!
Marcel van der Drift

14

Necromancing.
C'è un'ovvia soluzione semplice che non richiede le funzioni freccia (le funzioni freccia sono più lente del 30%) o metodi JIT tramite getter.
Quella soluzione è associare il contesto this nel costruttore.

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


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

Puoi scrivere un metodo autobind per associare automaticamente tutte le funzioni nel costruttore della 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

Nota che se non metti la funzione autobind nella stessa classe di una funzione membro, è giusto autoBind(this);e nonthis.autoBind(this);

Inoltre, la funzione autoBind sopra è disattivata, per mostrare il principio.
Se vuoi che funzioni in modo affidabile, devi verificare se la funzione è anche un getter / setter di una proprietà, perché altrimenti - boom - se la tua classe contiene proprietà, cioè.

Come questo:

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

Ho dovuto usare "autoBind (this)" non "this.autoBind (this)"
JohnOpincar

@ JohnOpincar: sì, this.autoBind (this) presume che l'autobind sia all'interno della classe, non come esportazione separata.
Stefan Steiger

Ora capisco. Metti il ​​metodo sulla stessa classe. L'ho inserito in un modulo "utility".
JohnOpincar

2

Nel tuo codice, hai provato a cambiare l'ultima riga come segue?

$(document).ready(() => thisTest.run());
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.