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?
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); } }
this
contexto en lugar de que cada sitio de llamada cree un nuevo cierre al invocar. this
contexto super.
Function.bind
También como se muestra:
$(document).ready(thisTest.run.bind(thisTest));
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));
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());