TypeScript “este” problema de scope cuando se llama en jquery callback

No estoy seguro del mejor enfoque para manejar el scope 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 un poco horrible. Significa que el código puede comstackrse y funcionar bien en algunas circunstancias, pero si olvidamos vincular el scope 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 scope de “esto”.

¿Alguna sugerencia?

Actualizar

Otro enfoque que funciona es usar la flecha adiposa:

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

¿Es ese un enfoque válido?

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

Encuadernación automática de clase
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 a métodos regulares, esto es exagerado. Sin embargo, si se usa mucho en las posiciones de callback, es más eficiente para la instancia de clase capturar this contexto en lugar de que cada sitio de llamada cree un nuevo cierre al invocar.
  • Bueno: Imposible que los llamadores externos olviden manejar this contexto
  • Bueno: TypeSafe en TypeScript
  • Bueno: sin trabajo adicional si la función tiene parámetros
  • Malo: las clases derivadas no pueden llamar a los métodos de la clase base escritos de esta manera usando super.
  • Malo: la semántica exacta de los métodos que están “vinculados previamente” y que no crean un contrato adicional no inseguro entre su clase y sus consumidores.

Function.bind
También como se muestra:

 $(document).ready(thisTest.run.bind(thisTest)); 
  • Bueno / malo: opuesto a compromiso de memoria / rendimiento en comparación con el primer método
  • Bueno: sin trabajo adicional si la función tiene parámetros
  • Malo: en TypeScript, esto actualmente no tiene ningún tipo de seguridad
  • Malo: solo disponible en ECMAScript 5, si eso te importa
  • Malo: debe escribir el nombre de la instancia dos veces

Flecha de grasa
En TypeScript (que se muestra aquí con algunos parámetros ficticios por razones explicativas):

 $(document).ready((n, m) => thisTest.run(n, m)); 
  • Bueno / malo: opuesto a compromiso de memoria / rendimiento en comparación con el primer método
  • Bueno: en TypeScript, esto tiene seguridad al 100%
  • Bueno: funciona en ECMAScript 3
  • Bueno: solo debe escribir el nombre de la instancia una vez
  • Malo: tendrás que escribir los parámetros dos veces
  • Malo: no funciona con varios parámetros

Nigromancia
Hay una solución simple obvia que no requiere funciones de flecha (las funciones de flecha son 30% más lentas), o métodos JIT a través de getters.
Esa solución es unir este contexto en el constructor.

 class DemonstrateScopingProblems { constructor() { this.run = this.run.bind(this); } private status = "blah"; public run() { alert(this.status); } } 

Puede usar este método para enlazar automáticamente todas las funciones de la clase en el constructor:

 class DemonstrateScopingProblems { constructor() { this.autoBind(this); } [...] } export function autoBind(self: any) { 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 

Otra solución que requiere una configuración inicial, pero que rinde frutos con su syntax ligera e indescifrable, literalmente, de una palabra, es el uso de Decoradores de métodos para enlazar métodos JIT a través de getters.

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

 class DemonstrateScopingProblems { private status = "blah"; @bound public run() { alert(this.status); } } 

No he visto esto mencionado en ningún lugar todavía, pero funciona sin problemas. Además, no hay una desventaja notable de este enfoque: la implementación de este decorador -incluida alguna verificación de tipo para la seguridad de tipo de tiempo de ejecución- es trivial y directa, y viene con una sobrecarga esencialmente cero después de la llamada al método inicial.

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 puede tomar un paso más, al hacer esto en un decorador de clases, iterando sobre los métodos y definiendo el descriptor de propiedades anterior para cada uno de ellos en una sola pasada.

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

 $(document).ready(() => thisTest.run());