TypeScript "this" проблема определения области видимости при вызове в обратном вызове jquery
Я не уверен в наилучшем подходе к обработке области "this" в TypeScript.
Вот пример общего шаблона в коде, который я конвертирую в 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);
Теперь я могу изменить вызов на...
$(document).ready(thisTest.run.bind(thisTest));
... который работает. Но это нечто ужасное. Это означает, что в некоторых случаях код может компилироваться и работать нормально, но если мы забудем связать область, она сломается.
Мне нужен способ сделать это внутри класса, так что при использовании класса нам не нужно беспокоиться о том, к чему привязан "this".
Любые предложения?
Update
Другим подходом, который работает, является использование стрелки жира:
class DemonstrateScopingProblems {
private status = "blah";
public run = () => {
alert(this.status);
}
}
Это действительный подход?
Ответы
Ответ 1
У вас есть несколько вариантов, каждый из которых имеет свои собственные компромиссы. К сожалению, нет очевидного лучшего решения, и это будет действительно зависеть от приложения.
Автоматическая привязка класса
Как показано в вашем вопросе:
class DemonstrateScopingProblems {
private status = "blah";
public run = () => {
alert(this.status);
}
}
- Хорошее/плохое: это создает дополнительное закрытие по методу на один экземпляр вашего класса. Если этот метод обычно используется только в обычных вызовах метода, это избыток. Однако, если он много использовал в позициях обратного вызова, более эффективно для экземпляра класса захватывать контекст
this
вместо каждого сайта вызова, создавая новое замыкание при вызове.
- Хорошо: невозможно для внешних вызывающих абонентов забыть обработать контекст
this
- Хорошо: Типы в TypeScript
- Хорошо: нет дополнительной работы, если функция имеет параметры
- Плохо: производные классы не могут называть методы базового класса, написанные таким образом, используя
super.
- Плохо: точная семантика методов "предварительно привязана" и которые не создают дополнительный нефинансовый контракт между вашим классом и его потребителями.
Function.bind
Также, как показано:
$(document).ready(thisTest.run.bind(thisTest));
- Хорошо/плохо: Противоположный обмен памяти/производительности по сравнению с первым методом.
- Хорошо: нет дополнительной работы, если функция имеет параметры
- Плохо: в TypeScript у этого в настоящее время нет безопасности типа
- Плохо: доступно только в ECMAScript 5, если это имеет значение для вас
- Плохо: вам нужно дважды ввести имя экземпляра
Толстая стрелка
В TypeScript (здесь показаны некоторые фиктивные параметры по объясняющим причинам):
$(document).ready((n, m) => thisTest.run(n, m));
- Хорошо/плохо: Противоположный обмен памяти/производительности по сравнению с первым методом.
- Хорошо: в TypeScript у этого есть безопасность на 100%
- Хорошо: работает в ECMAScript 3
- Хорошо: вам нужно только раз вводить имя экземпляра
- Плохо: вам придется вводить параметры дважды
- Плохо: не работает с переменными параметрами
Ответ 2
Другое решение, которое требует некоторой начальной настройки, но окупается непобедимым, буквально синтаксисом из одного слова, использует методы декораторов для JIT-связывания методов через геттеры.
Я создал репозиторий на GitHub, чтобы продемонстрировать реализацию этой идеи (она немного длинна, чтобы вписаться в ответ с его 40 строками кода, включая комментарии), который вы бы использовали так же просто, как:
class DemonstrateScopingProblems {
private status = "blah";
@bound public run() {
alert(this.status);
}
}
Я еще нигде не упоминал об этом, но он работает безупречно. Кроме того, нет заметного недостатка в этом подходе: реализация этого декоратора, включая некоторую проверку типов для безопасности типов во время выполнения, тривиальна и проста, и после начального вызова метода имеет практически нулевые издержки.
Важной частью является определение следующего метода get для прототипа класса, который выполняется непосредственно перед первым вызовом:
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];
}
Полный источник
Идея также может быть продвинута на один шаг, сделав это в декораторе класса, перебирая методы и определяя описанный выше дескриптор свойства для каждого из них за один проход.
Ответ 3
Necromancing.
Существует очевидное простое решение, которое не требует функций-стрелок (функции-стрелки на 30% медленнее) или методов JIT через геттеры.
Это решение состоит в том, чтобы связать контекст this в конструкторе.
class DemonstrateScopingProblems
{
constructor()
{
this.run = this.run.bind(this);
}
private status = "blah";
public run() {
alert(this.status);
}
}
Вы можете написать метод автоматической привязки для автоматической привязки всех функций в классе в конструкторе:
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
Обратите внимание, что если вы не поместите функцию autobind в тот же класс, что и функция-член, она будет просто autoBind(this);
, а не this.autoBind(this);
А также, вышеприведенная функция autoBind отключена, чтобы показать принцип.
Если вы хотите, чтобы это работало надежно, вам нужно проверить, является ли функция также получателем/установщиком свойства, потому что в противном случае - boom - если ваш класс содержит свойства, то есть.
Вот так:
export function autoBind(self: any) : any
{
for (const key of Object.getOwnPropertyNames(self.constructor.prototype))
{
if (key !== 'constructor')
{
// console.log(key);
let isFunction = true;
let desc = Object.getOwnPropertyDescriptor(self.constructor.prototype, key);
if (desc.get != null)
{
desc.get = desc.get.bind(self);
isFunction = false;
}
if (desc.set != null)
{
desc.set = desc.set.bind(self);
isFunction = false;
}
// const val = self[key]; // NO ! key could be a property !
if (isFunction && typeof(self[key]) === 'function')
{
let val = self[key];
self[key] = val.bind(self);
}
} // End if (key !== 'constructor' && typeof val === 'function')
} // Next key
return self;
} // End Function autoBind
Ответ 4
В вашем коде вы пытались просто изменить последнюю строку следующим образом?
$(document).ready(() => thisTest.run());