Проблема с областью действия 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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.