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());