
Foto: Curiosa Liliana Saeb (CC BY 2.0)
JavaScript é uma linguagem de vários paradigmas que suporta programação orientada a objetos e vinculação dinâmica. A vinculação dinâmica é um conceito poderoso que permite alterar a estrutura do código JavaScript em tempo de execução, mas esse poder e flexibilidade adicionais são obtidos à custa de alguma confusão, a maioria relacionada a this
comportamento no JavaScript.
Ligação dinâmica
Com a ligação dinâmica, a definição do método a ser chamado ocorre no tempo de execução e não no tempo de compilação. O JavaScript faz isso com this
e com a cadeia de protótipos. Em particular, dentro do método, this
é determinado durante a chamada e o valor this
será diferente dependendo de como o método foi definido.
Vamos jogar um jogo. Eu a chamo de "O que é this
aqui?"
const a = { a: 'a' }; const obj = { getThis: () => this, getThis2 () { return this; } }; obj.getThis3 = obj.getThis.bind(obj); obj.getThis4 = obj.getThis2.bind(obj); const answers = [ obj.getThis(), obj.getThis.call(a), obj.getThis2(), obj.getThis2.call(a), obj.getThis3(), obj.getThis3.call(a), obj.getThis4(), obj.getThis4.call(a) ];
Pense em quais answers
os valores na matriz de answers
e verifique suas respostas com console.log()
. Adivinhou?
Vamos começar com o primeiro caso e continuar em ordem. obj.getThis()
retorna undefined
, mas por quê? As funções de seta nunca têm this
próprio. Em vez disso, eles sempre tiram this
do escopo lexical ( aprox. Lexical isso ). Para a raiz do módulo ES6, a região lexical terá um valor undefined
this
. obj.getThis.call(a)
também não está definido pelo mesmo motivo. Para funções de seta, this
não pode ser substituído, mesmo com .call()
ou .bind()
. this
sempre será retirado do domínio lexical.
obj.getThis2()
obtém a ligação durante a chamada do método. Se não havia essa ligação para essa função antes, isso pode ser vinculado a this
(já que essa não é uma função de seta). Nesse caso, this
é um objeto obj
vinculado no momento em que o método é chamado .
ou sintaxe de acesso à propriedade [squareBracket]
. ( observe ligação implícita )
obj.getThis2.call(a)
pouco mais complicado. O método call()
chama uma função com esse valor fornecido e argumentos opcionais. Em outras palavras, o método obtém a ligação this
do parâmetro .call()
, então obj.getThis2.call(a)
retorna o objeto a
. ( observe ligação explícita )
No caso de obj.getThis3 = obj.getThis.bind(obj);
estamos tentando obter uma função de seta com this
limite, que, como já descobrimos, não funcionará, portanto, somos undefined
para obj.getThis3()
e obj.getThis3.call(a)
respectivamente.
Para métodos regulares, você pode vincular, então obj.getThis4()
retorna obj
, conforme o esperado. Ele já conseguiu sua ligação aqui obj.getThis4 = obj.getThis2.bind(obj);
, e obj.getThis4.call(a)
leva em consideração a primeira ligação e retorna obj
vez de a
.
Bola torcida
Resolveremos o mesmo problema, mas desta vez usamos uma class
com campos públicos para descrever o objeto (as inovações do Estágio 3 no momento da redação deste texto estão disponíveis no Chrome por padrão e com @babel/plugin-offer-class-properties
):
class Obj { getThis = () => this getThis2 () { return this; } } const obj2 = new Obj(); obj2.getThis3 = obj2.getThis.bind(obj2); obj2.getThis4 = obj2.getThis2.bind(obj2); const answers2 = [ obj2.getThis(), obj2.getThis.call(a), obj2.getThis2(), obj2.getThis2.call(a), obj2.getThis3(), obj2.getThis3.call(a), obj2.getThis4(), obj2.getThis4.call(a) ];
Pense nas respostas antes de continuar.
Você está pronta?
Todas as chamadas, exceto obj2.getThis2.call(a)
retornam uma instância do objeto. obj2.getThis2.call(a)
retorna a
. As funções de seta ainda conseguem this
em seu ambiente lexical. Há uma diferença em como this
é definido no ambiente lexical para propriedades de classe. Por dentro, a inicialização das propriedades da classe é mais ou menos assim:
class Obj { constructor() { this.getThis = () => this; } ...
Em outras palavras, a função de seta é definida dentro do contexto do construtor. Como essa é uma classe, a única maneira de criar uma instância é usar a new
palavra-chave (a omissão de new
resultará em erro). Uma das coisas mais importantes que a new
palavra-chave faz é criar uma nova instância do objeto e vincular this
a ele no construtor. Esse comportamento, combinado com outros comportamentos que mencionamos acima, deve explicar o resto.
Conclusão
Você conseguiu? Uma boa compreensão de como this
se comporta no JavaScript poupará muito tempo na depuração de problemas complexos. Se você cometeu um erro nas respostas, isso significa que você precisa praticar um pouco. Pratique com exemplos, depois volte e teste a si mesmo novamente até poder executar o teste e explicar a outra pessoa por que os métodos retornam o que retornam.
Se foi mais difícil do que você esperava, então você não está sozinho. Perguntei a muitos desenvolvedores sobre esse tópico e acho que até agora apenas um deles lidou com essa tarefa.
O que começou como uma pesquisa de métodos dinâmicos que você pode redirecionar com .call()
, .bind()
ou .apply()
se tornou muito mais complicado com a adição de métodos de classe e funções de seta. Talvez você deva se concentrar mais uma vez nisso. Lembre-se de que as funções de seta sempre tiram this
do escopo lexical e class
são lexicamente limitadas pelo construtor de classes sob o capô. Se você duvidar this
, lembre-se de que pode usar um depurador para verificar seu valor.
Lembre-se de que, ao resolver muitas tarefas JavaScript, você pode fazer this
sem this
. Na minha experiência, quase tudo pode ser redefinido em termos de funções puras que levam todos os argumentos usados como parâmetros explícitos ( this
pode ser pensado como uma variável implícita). A lógica descrita através de funções puras é determinística, o que a torna mais testável. Além disso, com essa abordagem, não há efeitos colaterais, portanto, diferentemente dos momentos de manipulação, é improvável que você quebre alguma coisa. Toda vez que this
definido, algo dependendo do seu valor pode quebrar.
No entanto, às vezes this
é útil. Por exemplo, para trocar métodos entre um grande número de objetos. Mesmo na programação funcional, this
pode ser usado para acessar outros métodos de objetos, a fim de implementar as transformações algébricas necessárias para construir novas álgebras sobre as já existentes. Portanto, universal .flatMap()
pode ser obtido usando this.map()
e this.constructor.of()
.
Obrigado pela ajuda na tradução de wksmirnowa e VIBaH_dev