Como entender a palavra-chave this e o contexto em JavaScript

Conforme mencionado em um de meus artigos anteriores, dominar totalmente o JavaScript pode ser uma jornada longa. Você pode ter se encontrado thisem sua jornada como desenvolvedor de JavaScript. Quando comecei, vi pela primeira vez ao usar eventListenerse com o jQuery. Mais tarde, tive que usá-lo frequentemente com o React e tenho certeza que você também fez. Isso não significa que eu realmente entendi o que é e como assumi-lo totalmente.

No entanto, é muito útil dominar o conceito por trás disso e, quando abordado com uma mente clara, também não é muito difícil.

Investigando nisso

Explicar thispode causar muita confusão, simplesmente pela nomenclatura da palavra-chave.

thisestá intimamente ligado ao contexto em que você está, em seu programa. Vamos começar pelo topo. Em nosso navegador, se você apenas digitar thisno console, obterá o window-objeto, o contexto mais externo para o seu JavaScript. No Node.js, se fizermos:

console.log(this)

acabamos com {}um objeto vazio. Isso é um pouco estranho, mas parece que o Node.js se comporta dessa forma. Se você fizer

(function() { console.log(this); })();

entretanto, você receberá o globalobjeto, o contexto mais externo. Nesse contexto setTimeout, setIntervalsão armazenados. Sinta-se à vontade para brincar um pouco com ele para ver o que você pode fazer com ele. A partir daqui, quase não há diferença entre o Node.js e o navegador. Eu estarei usando window. Lembre-se de que no Node.js será o globalobjeto, mas isso realmente não faz diferença.

Lembre-se: o contexto só faz sentido dentro das funções

Imagine que você escreve um programa sem aninhar nada nas funções. Você simplesmente escreveria uma linha após a outra, sem descer em estruturas específicas. Isso significa que você não precisa saber onde está. Você está sempre no mesmo nível.

Quando você começa a ter funções, você pode ter diferentes níveis de seu programa e thisrepresenta onde você está, qual objeto chamou a função.

Acompanhar o objeto chamador

Vamos dar uma olhada no exemplo a seguir e ver como thismuda dependendo do contexto:

const coffee = { strong: true, info: function() { console.log(`The coffee is ${this.strong ? '' : 'not '}strong`) }, } coffee.info() // The coffee is strong

Como chamamos uma função que é declarada dentro do coffeeobjeto, nosso contexto muda exatamente para esse objeto. Agora podemos acessar todas as propriedades desse objeto por meio de this. Em nosso exemplo acima, também podemos referenciá-lo diretamente fazendo coffee.strong. Fica mais interessante quando não sabemos em que contexto, em que objeto estamos ou quando as coisas simplesmente ficam um pouco mais complexas. Dê uma olhada no seguinte exemplo:

const drinks = [ { name: 'Coffee', addictive: true, info: function() { console.log(`${this.name} is ${this.addictive ? '' : 'not '} addictive.`) }, }, { name: 'Celery Juice', addictive: false, info: function() { console.log(`${this.name} is ${this.addictive ? '' : 'not '} addictive.`) }, }, ] function pickRandom(arr) { return arr[Math.floor(Math.random() * arr.length)] } pickRandom(drinks).info()

Classes e instâncias

As classes podem ser usadas para abstrair seu código e compartilhar o comportamento. Sempre repetir a infodeclaração de função no último exemplo não é bom. Como as classes e suas instâncias são de fato objetos, elas se comportam da mesma maneira. Uma coisa a se notar, entretanto, é que declarar thisno construtor na verdade é uma previsão para o futuro, quando haverá uma instância.

Vamos dar uma olhada:

class Coffee { constructor(strong) { this.strong = !!strong } info() { console.log(`This coffee is ${this.strong ? '' : 'not '}strong`) } } const strongCoffee = new Coffee(true) const normalCoffee = new Coffee(false) strongCoffee.info() // This coffee is strong normalCoffee.info() // This coffee is not strong

Armadilha: chamadas de função perfeitamente aninhadas

Às vezes, acabamos em um contexto que realmente não esperávamos. Isso pode acontecer, quando involuntariamente chamamos a função dentro de outro contexto de objeto. Um exemplo muito comum é ao usar setTimeoutou setInterval:

// BAD EXAMPLE const coffee = { strong: true, amount: 120, drink: function() { setTimeout(function() { if (this.amount) this.amount -= 10 }, 10) }, } coffee.drink()

O que você acha que coffee.amounté?

...

..

.

Ainda está 120. Primeiro, estávamos dentro do coffeeobjeto, já que o drinkmétodo é declarado dentro dele. Nós apenas fizemos setTimeoute nada mais. É exatamente isso.

Como expliquei anteriormente, o setTimeoutmétodo é realmente declarado no windowobjeto. Ao chamá-lo, na verdade mudamos o contexto para o windownovamente. Isso significa que nossas instruções realmente tentaram mudar window.amount, mas acabaram não fazendo nada por causa da ifinstrução-. Para cuidar disso, temos bindnossas funções (veja abaixo).

Reagir

Usando o React, espero que isso seja coisa do passado em breve, graças a Hooks. No momento, ainda temos de bindtudo (mais sobre isso depois) de uma forma ou de outra. Quando eu comecei, não tinha ideia do por que estava fazendo isso, mas a essa altura, você já deve saber por que é necessário.

Vamos dar uma olhada em dois componentes simples da classe React:

// BAD EXAMPLE import React from 'react' class Child extends React.Component { render() { return  Get some Coffee!  } } class Parent extends React.Component { constructor(props) { super(props) this.state = { coffeeCount: 0, } // change to turn into good example – normally we would do: // this._getCoffee = this._getCoffee.bind(this) } render() { return (    ) } _getCoffee() { this.setState({ coffeeCount: this.state.coffeeCount + 1, }) } }

Quando agora clicarmos no botão renderizado pelo Child, receberemos um erro. Por quê? Porque o React mudou nosso contexto ao chamar o _getCoffeemétodo.

Presumo que o React chame o método render de nossos componentes em outro contexto, por meio de classes auxiliares ou semelhantes (embora eu tivesse que cavar mais fundo para descobrir com certeza). Portanto, this.stateestá indefinido e estamos tentando acessar this.state.coffeeCount. Você deve receber algo parecido Cannot read property coffeeCount of undefined.

Para resolver o problema, você tem que bind(chegaremos lá) os métodos em nossas classes, assim que os passarmos para fora do componente onde estão definidos.

Vamos dar uma olhada em mais um exemplo genérico:

// BAD EXAMPLE class Viking { constructor(name) { this.name = name } prepareForBattle(increaseCount) { console.log(`I am ${this.name}! Let's go fighting!`) increaseCount() } } class Battle { constructor(vikings) { this.vikings = vikings this.preparedVikingsCount = 0 this.vikings.forEach(viking => { viking.prepareForBattle(this.increaseCount) }) } increaseCount() { this.preparedVikingsCount++ console.log(`${this.preparedVikingsCount} vikings are now ready to fight!`) } } const vikingOne = new Viking('Olaf') const vikingTwo = new Viking('Odin') new Battle([vikingOne, vikingTwo])

Estamos passando increaseCountde uma aula para outra. Quando chamamos o increaseCountmétodo em Viking, já mudamos o contexto e thisrealmente apontamos para o Viking, o que significa que nosso increaseCountmétodo não funcionará como esperado.

Solução - ligar

A solução mais simples para nós são bindos métodos que serão transmitidos de nosso objeto ou classe original. Existem diferentes maneiras de vincular funções, mas a mais comum (também no React) é vinculá-las ao construtor. Portanto, teríamos que adicionar esta linha no Battleconstrutor antes da linha 18:

this.increaseCount = this.increaseCount.bind(this)

Você pode vincular qualquer função a qualquer contexto. Isso não significa que você sempre terá que vincular a função ao contexto em que foi declarada (no entanto, esse é o caso mais comum). Em vez disso, você pode vinculá-lo a outro contexto. Com bind, você sempre define o contexto para uma declaração de função . Isso significa que todas as chamadas para essa função receberão o contexto vinculado como this. Existem dois outros auxiliares para definir o contexto.

As funções de seta `() => {}` vinculam automaticamente a função ao contexto de declaração

Inscreva-se e ligue

They both do basically the same thing, just that the syntax is different. For both, you pass the context as first argument. apply takes an array for the other arguments, with call you can just separate other arguments by comma. Now what do they do? Both of these methods set the context for one specific function call. When calling the function without call , the context is set to the default context (or even a bound context). Here is an example:

class Salad { constructor(type) { this.type = type } } function showType() { console.log(`The context's type is ${this.type}`) } const fruitSalad = new Salad('fruit') const greekSalad = new Salad('greek') showType.call(fruitSalad) // The context's type is fruit showType.call(greekSalad) // The context's type is greek showType() // The context's type is undefined

Can you guess what the context of the last showType() call is?

..

.

You’re right, it is the outermost scope, window . Therefore, type is undefined, there is no window.type

É isso, espero que agora você tenha um entendimento claro sobre como usar thisem JavaScript. Fique à vontade para deixar sugestões para o próximo artigo nos comentários.

Sobre o autor: Lukas Gisder-Dubé cofundou e liderou uma startup como CTO por 1 ano e meio, construindo a equipe de tecnologia e arquitetura. Depois de deixar a startup, ele ensinou programação como instrutor líder na Ironhack e agora está construindo uma agência e consultoria de startups em Berlim. Confira dube.io para saber mais.