TypeScript "este" problema de alcance cuando se llama en la devolución de llamada de jquery

107

No estoy seguro del mejor enfoque para manejar el alcance de "esto" en TypeScript.

Aquí hay un ejemplo de un patrón común en el código que estoy convirtiendo a 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); 

Ahora, podría cambiar la llamada a ...

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

... que funciona. Pero es algo horrible. Significa que todo el código puede compilarse y funcionar bien en algunas circunstancias, pero si nos olvidamos de vincular el alcance, se romperá.

Me gustaría una forma de hacerlo dentro de la clase, de modo que cuando usemos la clase no tengamos que preocuparnos por el alcance de "esto".

¿Alguna sugerencia?

Actualizar

Otro enfoque que funciona es usar la flecha gruesa:

class DemonstrateScopingProblems {
    private status = "blah";

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

¿Es ese un enfoque válido?

Jonathan Moffatt
fuente
2
Esto sería útil: youtube.com/watch?v=tvocUcbCupA
basarat
Nota: Ryan copió su respuesta en TypeScript Wiki .
Franklin Yu
Busque aquí una solución TypeScript 2+.
Deilan

Respuestas:

166

Aquí tiene algunas opciones, cada una con sus propias compensaciones. Desafortunadamente, no existe una mejor solución obvia y realmente dependerá de la aplicación.

Vinculación automática de clases
Como se muestra en su pregunta:

class DemonstrateScopingProblems {
    private status = "blah";

    public run = () => {
        alert(this.status);
    }
}
  • Bueno / malo: esto crea un cierre adicional por método por instancia de su clase. Si este método generalmente solo se usa en llamadas de método regulares, esto es excesivo. Sin embargo, si se usa mucho en posiciones de devolución de llamada, es más eficiente para la instancia de clase capturar el thiscontexto en lugar de que cada sitio de llamada cree un nuevo cierre al invocarlo.
  • Bueno: es imposible que los llamantes externos se olviden de manejar el thiscontexto
  • Bueno: Typesafe en TypeScript
  • Bueno: no hay trabajo adicional si la función tiene parámetros
  • Malo: las clases derivadas no pueden llamar a los métodos de clase base escritos de esta manera usando super.
  • Malo: La semántica exacta de qué métodos están "pre-enlazados" y cuáles no crean un contrato adicional sin seguridad de tipos entre su clase y sus consumidores.

Function.bind
También como se muestra:

$(document).ready(thisTest.run.bind(thisTest));
  • Bueno / malo: compensación opuesta de memoria / rendimiento en comparación con el primer método
  • Bueno: no hay trabajo adicional si la función tiene parámetros
  • Malo: en TypeScript, esto actualmente no tiene seguridad de tipos
  • Malo: solo disponible en ECMAScript 5, si eso le importa
  • Malo: tienes que escribir el nombre de la instancia dos veces

Flecha
gruesa en TypeScript (que se muestra aquí con algunos parámetros ficticios por razones explicativas):

$(document).ready((n, m) => thisTest.run(n, m));
  • Bueno / malo: compensación opuesta de memoria / rendimiento en comparación con el primer método
  • Bueno: en TypeScript, esto tiene un 100% de seguridad de tipos
  • Bueno: funciona en ECMAScript 3
  • Bueno: solo tienes que escribir el nombre de la instancia una vez
  • Malo: tendrás que escribir los parámetros dos veces
  • Malo: no funciona con parámetros variadic
Ryan Cavanaugh
fuente
1
+1 Gran respuesta Ryan, me encanta el desglose de los pros y los contras, ¡gracias!
Jonathan Moffatt
- En tu Function.bind, creas un nuevo cierre cada vez que necesitas adjuntar el evento.
131
1
¡¡La flecha gorda acaba de hacerlo !! : D: D = () => ¡Muchas gracias! : D
Christopher Stock
@ ryan-cavanaugh ¿qué pasa con lo bueno y lo malo en términos de cuándo se liberará el objeto? Como en el ejemplo de un SPA que está activo durante> 30 minutos, ¿cuál de los anteriores es mejor para que lo manejen los recolectores de basura de JS?
abbaf33f
Todos estos serían liberables cuando la instancia de clase sea liberable. Los dos últimos se podrán liberar antes si la vida útil del controlador de eventos es más corta. Sin embargo, en general, diría que no habrá una diferencia mensurable.
Ryan Cavanaugh
16

Otra solución que requiere una configuración inicial pero que vale la pena con su sintaxis invencible, literalmente de una palabra, es usar Decoradores de métodos para enlazar métodos JIT a través de getters.

Creé un repositorio en GitHub para mostrar una implementación de esta idea (es un poco largo para encajar en una respuesta con sus 40 líneas de código, incluidos los comentarios) , que usaría tan simplemente como:

class DemonstrateScopingProblems {
    private status = "blah";

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

Todavía no he visto esto mencionado en ninguna parte, pero funciona perfectamente. Además, este enfoque no tiene una desventaja notable: la implementación de este decorador, incluida la verificación de tipos para la seguridad de tipos en tiempo de ejecución , es trivial y sencilla, y tiene una sobrecarga esencialmente cero después de la llamada inicial al método.

La parte esencial es definir el siguiente getter en el prototipo de clase, que se ejecuta inmediatamente antes de la primera llamada:

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

Fuente completa


La idea también se puede llevar un paso más allá, haciendo esto en un decorador de clases, iterando sobre los métodos y definiendo el descriptor de propiedad anterior para cada uno de ellos en una sola pasada.

John Weisz
fuente
¡justo lo que necesitaba!
Marcel van der Drift
14

Nigromante.
Existe una solución simple y obvia que no requiere funciones de flecha (las funciones de flecha son un 30% más lentas) o métodos JIT a través de getters.
Esa solución es vincular el contexto this en el constructor.

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


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

Puede escribir un método de autobind para vincular automáticamente todas las funciones en el constructor de la clase:

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

Tenga en cuenta que si no coloca la función autobind en la misma clase que una función miembro, es solo autoBind(this);y nothis.autoBind(this);

Y también, la función autoBind anterior está simplificada, para mostrar el principio.
Si desea que esto funcione de manera confiable, debe probar si la función también es un getter / setter de una propiedad, porque de lo contrario, boom, si su clase contiene propiedades, eso es.

Me gusta esto:

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
fuente
Tuve que usar "autoBind (this)" no "this.autoBind (this)"
JohnOpincar
@JohnOpincar: sí, this.autoBind (esto) asume que el enlace automático está dentro de la clase, no como una exportación separada.
Stefan Steiger
Entiendo ahora. Pones el método en la misma clase. Lo puse en un módulo de "utilidad".
JohnOpincar
2

En su código, ¿ha intentado simplemente cambiar la última línea de la siguiente manera?

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