JavaScript

4 jun, 2018

Javascript: entendendo o this de uma vez por todas!

Publicidade

Desde que programamos utilizando linguagens de alto nível, a keyword this está presente em nossas vidas. Este “pronome” é utilizado quando precisamos nos referir ao objeto atual, da mesma forma como utilizamos pronomes em nossa língua falada. Por exemplo, se dissermos: “João está correndo porque ele precisa pegar o trem”, veja que não utilizamos o nome João duas vezes, mas poderíamos ter dito: “João está correndo porque João precisa pegar o trem”. Ambas estão corretas, mas veja como nos repetimos.

O mesmo acontece quando estamos utilizando this em um código. Em algumas linguagens, o this possui um contexto fixo e definido, já no JavaScript, isso não é bem assim; e é justamente aí que a maioria dos programadores JavaScript, sejam eles iniciantes ou bem experientes, começam a ter grandes confusões. Para entender o this, precisamos entender algo que chamamos de contexto.

Contexto

O contexto é todo o escopo que determinada função, método, objeto ou variável está inserida, por exemplo:

let pessoa = 'João'
console.log(this.pessoa) // João

No exemplo anterior, temos um contexto global. Todo o objeto JavaScript está inserido no contexto de window, que seria o contexto global da aplicação.

Agora vamos fazer uma alteração no nosso código:

let nome = 'João'

function dizerFrase() {
  console.log(`${this.nome} está correndo para pegar o trem!`)
}

dizerFrase() // João está correndo para pegar o trem!

let pedro = {
  nome: 'Pedro',
  dizerFrase: function () {
    console.log(`${this.nome} está correndo para pegar o trem!`)
  }
}

pedro.dizerFrase() // Pedro está correndo para pegar o trem!

Percebam que a chamada que fazemos é exatamente a mesma; estamos buscando valores de this.nome, mas dependendo de como chamamos o método dizerFrase(), este nome é diferente. Isto é chamado de contexto dinâmico. Isso significa que nossa primeira chamada tem o this, sendo como window.nome e o segundo como pedro.nome.

Por que this?

O this não é só um facilitador (ou um complicador), mas também agrega um valor semântico ao código, assim como “ele” agrega um valor semântico à frase que falamos anteriormente. Se fossemos obrigados a repetir em todas as chamadas o nome do objeto que queremos referenciar, poderíamos acabar com conflitos de nomenclatura.

Ao criar várias variáveis em arquivos e projetos distintos, vamos cair em um problema, onde nosso código pode tentar executar uma propriedade de uma variável global sem que saibamos. Isso causa um aumento significativo na dificuldade de debug da nossa aplicação, pois vamos ter que adivinhar qual seria essa variável global que estamos chamando.

Portanto, o uso do this não é só estético, mas ajuda a entender que estamos querendo acessar a propriedade de um objeto anterior, assim como “ele” nos dizia que estamos querendo dizer o nome da pessoa citada anteriormente na sentença.

Usos de this

Podemos separar os usos e dificuldades em algumas classes. No geral, this sempre levará o valor do contexto superior, ou seja, o valor daquele objeto que o invocou. Por exemplo:

let nome = 'João'

function dizerFrase() {
  console.log(`${this.nome} está correndo para pegar o trem!`)
}

dizerFrase() // João está correndo para pegar o trem!

let pedro = {
  nome: 'Pedro',
  dizerFrase: function () {
    console.log(`${this.nome} está correndo para pegar o trem!`)
  }
}

pedro.dizerFrase() // Pedro está correndo para pegar o trem!

Perceba que, na primeira execução, this toma o valor de window, pois foi esse objeto que o invocou. Já na segunda invocação, veja que estamos utilizando pedro.dizerFrase(), o que significa que o objeto invocador é pedro, portanto, o contexto de this será o valor de pedro.

Para entender melhor o conceito, vamos usar uma analogia: temos uma banda e queremos tocar músicas de diferentes artistas para diferentes contextos de pessoas.

O contexto global

Imagine que queremos tocar uma música para todos ouvirem, que não tenha restrições de idade ou estilo, então vamos fazer algo assim:

var artista = 'The Beatles'

function tocar () {
    let instrumentos = ['baixo', 'guitarra', 'microfone', 'bateria']
    console.log(`Estamos tocando ${this.artist}!`)
}

tocar() // Estamos tocando The Beatles!

Neste caso, como já mostramos anteriormente, o contexto de execução da função é determinado pelo invocador da mesma, no caso window portanto, this vai assumir o valor de “The Beatles”.

O contexto local

Agora vamos deixar nossa banda mais específica. Fomos convidados para tocar em um bar, mas este bar só aceita Rock dos anos 70. Então temos que diversificar nosso repertório. Vamos criar nosso objeto bar:

var artista = 'The Beatles'

function tocar () {
    let instrumentos = ['baixo', 'guitarra', 'microfone', 'bateria']
    console.log(`Estamos tocando ${this.artista}!`)
}

tocar() // Estamos tocando The Beatles!

let bar = {
    artista: 'Dire Straits',
    tocar: function () {
        let instrumentos = ['baixo', 'guitarra', 'bateria', 'teclados', 'microfone']
        console.log(`Vamos tocar ${this.artista}!`)
    }
}

bar.tocar() // Vamos tocar Dire Straits!

Agora temos um contexto mais específico para a nossa função.

Alterando o contexto

O que vimos até agora, não foi nada mais do que uma repetição do que tínhamos visto anteriormente, mas agora fomos chamados para tocar várias músicas em um show, com um público mais eclético, ou seja, vamos ter que usar todos os nossos repertórios e também adicionar mais um!

var artista = 'The Beatles'

function tocar () {
    console.log(`Estamos tocando ${this.artista}!`)
}

let bar = {
    artista: 'Dire Straits',
    tocar: function () {
        console.log(`Vamos tocar ${this.artista}!`)
    }
}

let show = {
    artista: 'Blind Guardian',
    tocar: function () {
        console.log(`Vamos tocar ${this.artista}`)
    }
}

tocar() // Estamos tocando The Beatles!
bar.tocar() // Vamos tocar Dire Straits!
show.tocar() // Vamos tocar Blind Guardian!

Podemos simplesmente chamar três vezes a função tocar() para três contextos diferentes (como fizemos acima), não é mesmo? Mas isso é muita repetição de código! Vamos fazer algumas alterações:

var artista = 'The Beatles'

let bar = {
    artista: 'Dire Straits'
}

let show = {
    artista: 'Blind Guardian'
}

function tocar () { 
    console.log(`Estamos tocando ${this.artista}!`)
}

Agora podemos alternar os contextos utilizando a função bind. O que ela faz é basicamente aplicar um dado this à função que a está chamando através de métodos como o call e o apply:

var artista = 'The Beatles'

let bar = {
    artista: 'Dire Straits'
}

let show = {
    artista: 'Blind Guardian'
}

function tocar () { 
    console.log(`Estamos tocando ${this.artista}!`)
}

let tocarParaTodos = tocar // Usamos a função com o contexto global
let tocarEmBar = tocar.bind(bar) // Alteramos o contexto para o bar
let tocarEmShow = tocar.bind(show) // Alteramos o contexto para o show

tocarParaTodos() // Estamos tocando The Beatles!
tocarEmBar() // Estamos tocando Dire Straits!
tocarEmShow() // Estamos tocando Blind Guardian!

Contexto em arrow functions

Se simplesmente alterarmos a nossa função anônima para uma arrow function, vamos ter uma saída diferente:

var artista = 'The Beatles'

let bar = {
    artista: 'Dire Straits'
}

let show = {
    artista: 'Blind Guardian'
}

let tocar () => { 
    console.log(`Estamos tocando ${this.artista}!`)
}

let tocarParaTodos = tocar // Usamos a função com o contexto global
let tocarEmBar = tocar.bind(bar) // Alteramos o contexto para o bar
let tocarEmShow = tocar.bind(show) // Alteramos o contexto para o show

tocarParaTodos() // Estamos tocando The Beatles!
tocarEmBar() // Estamos tocando The Beatles!
tocarEmShow() // Estamos tocando The Beatles!

Isso é o que chamamos de contexto léxico; uma arrow function, diferentemente de uma função anônima comum, obtém seu contexto a partir do contexto superior, ou seja, ao invés de termos this referenciando nosso objeto do bind, vamos ter ambos referenciando o contexto onde o objeto chamado está inserido, que é o window.

Perceba que o bind não faz diferença por aqui, porque ela sempre irá obter o contexto do objeto que está mais acima da cadeia. No caso, seria o window. Independentemente se utilizarmos os métodos de mais baixo nível desta forma:

var artista = 'The Beatles'

let bar = {
    artista: 'Dire Straits'
}

let show = {
    artista: 'Blind Guardian'
}

let tocar () => { 
    console.log(`Estamos tocando ${this.artista}!`)
}

let tocarParaTodos = tocar // Usamos a função com o contexto global

tocarParaTodos() // Estamos tocando The Beatles!
tocar.call(bar) // Estamos tocando The Beatles!
tocar.apply(show) // Estamos tocando The Beatles!

Quando utilizamos call e apply – funções que nos permitem alterar o contexto, assim como o bind, mas ao mesmo tempo passar argumentos para estas funções – usando arrow functions, o valor do que seria o argumento this é completamente ignorado. Na documentação do MDN, inclusive, há uma declaração dizendo que o valor deve ser null.

Este comportamento das arrow functions foi uma das mais esperadas funcionalidades a serem lançadas na linguagem, pois antes era necessário que salvássemos o contexto anterior em um escopo global para que a função tivesse o escopo léxico superior, desta forma:

var artista = 'The Beatles'
var self = this // Salvando o contexto global

let bar = {
    artista: 'Dire Straits',
    tocar: function () { console.log(`Estamos tocando ${self.artista}!`) }
}

function tocar () { 
    console.log(`Estamos tocando ${this.artista}!`)
}

let tocarParaTodos = tocar // Usamos a função com o contexto global

tocar() // Estamos tocando The Beatles!
bar.tocar() // Estamos tocando The Beatles!

Conclusão

Juntando todos estes exemplos, conseguimos verificar como o this se comporta no JavaScript e também entender qual é o contexto que as nossas funções devem receber em cada um destes casos. Agora não há mais motivo para termos dúvidas sobre como e quando devemos utilizar this, além disso, ainda vimos que arrow functions são muito mais do que melhores maneiras de declarar uma função, elas também alteram sua semântica e seu contexto!

Até a próxima!