Проблема с областью действия TypeScript при вызове в обратном вызове jquery

107

Я не уверен в лучшем подходе для обработки области видимости «этого» в 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));

... что действительно работает. Но это ужасно. Это означает, что весь код может компилироваться и работать нормально в некоторых обстоятельствах, но если мы забудем привязать область видимости, он сломается.

Мне нужен способ сделать это внутри класса, чтобы при использовании класса нам не нужно было беспокоиться о том, к чему относится «это».

Какие-либо предложения?

Обновить

Другой подход, который работает, - использование толстой стрелки:

class DemonstrateScopingProblems {
    private status = "blah";

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

Это правильный подход?

Джонатан Моффатт
источник
2
Это было бы полезно: youtube.com/watch?v=tvocUcbCupA
basarat
Примечание: Райан скопировал свой ответ в TypeScript Wiki .
Франклин Ю
Найдите здесь решение TypeScript 2+.
Deilan

Ответы:

166

У вас есть несколько вариантов, каждый со своими компромиссами. К сожалению, нет очевидного лучшего решения, и это действительно будет зависеть от приложения.

Автоматическая привязка классов
Как показано в вашем вопросе:

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
  • Хорошо: вам нужно ввести имя экземпляра только один раз
  • Плохо: вам придется вводить параметры дважды
  • Плохо: не работает с вариативными параметрами.
Райан Кавано
источник
1
+1 Отличный ответ Райан, люблю разбивку плюсов и минусов, спасибо!
Джонатан Моффатт
- В вашем Function.bind вы создаете новое закрытие каждый раз, когда вам нужно прикрепить событие.
131
1
Толстая стрела только что сделала это !! : D: D = () => Большое спасибо! : D
Кристофер Сток
@ ryan-cavanaugh как насчет хорошего и плохого с точки зрения того, когда объект будет освобожден? Как и в примере с SPA, активным более 30 минут, что из вышеперечисленного лучше всего обрабатывает сборщик мусора JS?
abbaf33f
Все это можно будет бесплатно, если освободить экземпляр класса. Последние два будут освобождены раньше, если время жизни обработчика событий будет короче. В целом, я бы сказал, что заметной разницы не будет.
Райан Кавано,
16

Другое решение, которое требует некоторой начальной настройки, но окупается своим непреодолимо легким синтаксисом буквально из одного слова, - это использование декораторов методов для JIT-связывания методов через геттеры.

Я создал репозиторий на GitHub, чтобы продемонстрировать реализацию этой идеи (это немного длинно, чтобы уместиться в ответ с его 40 строками кода, включая комментарии) , который вы могли бы использовать так же просто, как:

class DemonstrateScopingProblems {
    private status = "blah";

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

Я еще нигде не видел, чтобы это упоминалось, но работает безупречно. Кроме того, у этого подхода нет заметных недостатков: реализация этого декоратора - включая некоторую проверку типов для обеспечения безопасности типов во время выполнения - тривиальна и проста и требует практически нулевых накладных расходов после первоначального вызова метода.

Существенной частью является определение следующего получателя для прототипа класса, который выполняется непосредственно перед первым вызовом:

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

Полный исходный код


Идея также может быть продвинута на один шаг дальше, вместо этого выполняя это в декораторе класса, перебирая методы и определяя описанный выше дескриптор свойства для каждого из них за один проход.

Джон Вайс
источник
как раз то, что мне было нужно!
Марсель ван дер Дрифт,
14

Некромантинг.
Есть очевидное простое решение, которое не требует стрелочных функций (стрелочные функции на 30% медленнее) или JIT-методов через геттеры.
Это решение - привязать этот контекст в конструкторе.

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


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

Вы можете написать метод autobind для автоматического связывания всех функций в конструкторе класса:

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

Обратите внимание: если вы не поместите функцию autobind в тот же класс, что и функцию-член, это просто, autoBind(this);а неthis.autoBind(this);

Кроме того, указанная выше функция autoBind упрощена, чтобы показать принцип.
Если вы хотите, чтобы это работало надежно, вам необходимо проверить, является ли функция также получателем / установщиком свойства, иначе - бум - если ваш класс содержит свойства, то есть.

Как это:

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
Стефан Штайгер
источник
Мне пришлось использовать «autoBind (this)», а не «this.autoBind (this)»
JohnOpincar
@JohnOpincar: да, this.autoBind (this) предполагает, что автобинд находится внутри класса, а не как отдельный экспорт.
Stefan Steiger
Теперь я понимаю. Вы помещаете метод в тот же класс. Положил в "служебный" модуль.
JohnOpincar
2

Вы пытались изменить последнюю строку в своем коде следующим образом?

$(document).ready(() => thisTest.run());
Альбино Кордейро
источник