TypeScript “este” problema de escopo quando chamado no retorno de chamada jquery

107

Não tenho certeza da melhor abordagem para lidar com o escopo "this" no TypeScript.

Aqui está um exemplo de um padrão comum no código que estou convertendo para 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); 

Agora, eu poderia mudar a chamada para ...

$(document).ready(thisTest.run.bind(thisTest));

... que funciona. Mas é meio horrível. Isso significa que o código pode ser compilado e funcionar bem em algumas circunstâncias, mas se esquecermos de vincular o escopo, ele será interrompido.

Eu gostaria de uma maneira de fazer isso dentro da classe, de modo que, ao usar a classe, não precisemos nos preocupar com o escopo de "this".

Alguma sugestão?

Atualizar

Outra abordagem que funciona é usar a seta grande:

class DemonstrateScopingProblems {
    private status = "blah";

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

Essa é uma abordagem válida?

Jonathan Moffatt
fonte
2
Isso seria útil: youtube.com/watch?v=tvocUcbCupA
basarat
Nota: Ryan copiou sua resposta para TypeScript Wiki .
Franklin Yu
Procure aqui uma solução TypeScript 2+.
Deilan

Respostas:

166

Você tem algumas opções aqui, cada uma com suas próprias compensações. Infelizmente, não existe uma melhor solução óbvia e isso realmente dependerá do aplicativo.

Encadernação automática de classe,
conforme mostrado em sua pergunta:

class DemonstrateScopingProblems {
    private status = "blah";

    public run = () => {
        alert(this.status);
    }
}
  • Bom / ruim: Isso cria um encerramento adicional por método por instância de sua classe. Se esse método geralmente é usado apenas em chamadas de método regulares, isso é um exagero. No entanto, se for muito usado em posições de retorno de chamada, é mais eficiente para a instância da classe capturar o thiscontexto em vez de cada site de chamada criar um novo encerramento na chamada.
  • Bom: impossível para chamadores externos se esquecerem de lidar com o thiscontexto
  • Bom: proteção de tipos em TypeScript
  • Bom: nenhum trabalho extra se a função tiver parâmetros
  • Ruim: classes derivadas não podem chamar métodos de classe base escritos desta forma usando super.
  • Mau: a semântica exata de quais métodos são "pré-vinculados" e quais não são, cria um contrato adicional não seguro de tipo entre sua classe e seus consumidores.

Function.bind
Também como mostrado:

$(document).ready(thisTest.run.bind(thisTest));
  • Bom / ruim: troca de memória / desempenho oposta em comparação com o primeiro método
  • Bom: nenhum trabalho extra se a função tiver parâmetros
  • Ruim: no TypeScript, atualmente não há segurança de tipo
  • Ruim: disponível apenas no ECMAScript 5, se for importante para você
  • Ruim: você precisa digitar o nome da instância duas vezes

Seta grande
no TypeScript (mostrada aqui com alguns parâmetros fictícios por motivos explicativos):

$(document).ready((n, m) => thisTest.run(n, m));
  • Bom / ruim: troca de memória / desempenho oposta em comparação com o primeiro método
  • Bom: no TypeScript, tem 100% de segurança de tipo
  • Bom: Funciona em ECMAScript 3
  • Bom: você só precisa digitar o nome da instância uma vez
  • Ruim: você terá que digitar os parâmetros duas vezes
  • Ruim: não funciona com parâmetros variáveis
Ryan Cavanaugh
fonte
1
+1 Ótima resposta Ryan, adorei a análise dos prós e contras, obrigado!
Jonathan Moffatt
- Em seu Function.bind, você cria um novo encerramento toda vez que precisar anexar o evento.
131 de
1
A flecha gorda fez isso !! : D: D = () => Muito obrigado! : D
Christopher Stock
@ ryan-cavanaugh e quanto ao bom e ao mau em termos de quando o objeto será liberado? Como no exemplo de um SPA que está ativo por> 30 minutos, qual das opções acima é a melhor para os coletores de lixo JS lidarem?
abbaf33f
Todos eles seriam liberáveis ​​quando a instância da classe fosse liberável. Os dois últimos serão liberados mais cedo se o tempo de vida do manipulador de eventos for menor. Em geral, eu diria que não haverá uma diferença mensurável, no entanto.
Ryan Cavanaugh,
16

Outra solução que requer alguma configuração inicial, mas compensa com sua luz invencível, literalmente sintaxe de uma palavra, é usar os Decoradores de Método para vincular métodos JIT por meio de getters.

Eu criei um repositório no GitHub para mostrar uma implementação dessa ideia (é um pouco demorado para caber em uma resposta com suas 40 linhas de código, incluindo comentários) , que você usaria simplesmente como:

class DemonstrateScopingProblems {
    private status = "blah";

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

Eu não vi isso mencionado em nenhum lugar ainda, mas funciona perfeitamente. Além disso, não há nenhuma desvantagem notável nessa abordagem: a implementação desse decorador - incluindo alguma verificação de tipo para segurança de tipo em tempo de execução - é trivial e direta e vem com essencialmente zero sobrecarga após a chamada do método inicial.

A parte essencial é definir o seguinte getter no protótipo da classe, que é executado imediatamente antes da primeira chamada:

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

Fonte completa


A ideia também pode ser levada um passo adiante, fazendo isso em um decorador de classe, iterando os métodos e definindo o descritor de propriedade acima para cada um deles em uma passagem.

John Weisz
fonte
apenas o que eu precisava!
Marcel van der Drift
14

Necromante.
Há uma solução simples e óbvia que não requer funções de seta (as funções de seta são 30% mais lentas) ou métodos JIT por meio de getters.
Essa solução é vincular o contexto this no construtor.

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


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

Você pode escrever um método autobind para vincular automaticamente todas as funções no construtor da classe:

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

Observe que se você não colocar a função autobind na mesma classe que uma função membro, ela é justa autoBind(this);e nãothis.autoBind(this);

E também, a função autoBind acima é simplificada, para mostrar o princípio.
Se você deseja que isso funcione de forma confiável, você precisa testar se a função é um getter / setter de uma propriedade também, porque caso contrário - boom - se sua classe contém propriedades, é claro.

Como isso:

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
Stefan Steiger
fonte
Tive de usar "autoBind (this)" e não "this.autoBind (this)"
JohnOpincar
@JohnOpincar: sim, this.autoBind (this) assume que autobind está dentro da classe, não como uma exportação separada.
Stefan Steiger
Eu entendo agora. Você coloca o método na mesma classe. Eu coloquei em um módulo "utilitário".
JohnOpincar
2

Em seu código, você tentou apenas alterar a última linha da seguinte maneira?

$(document).ready(() => thisTest.run());
Albino Cordeiro
fonte