sábado, 25 de fevereiro de 2017

Entenda finalmente o this, bind(), call() e apply()


Olá, hoje vou abordar um assunto que pode confundir desenvolvedores que estão vindo de uma linguagem puramente orientada a objetos, para o Javascript a pouco tempo: o uso do this, e algumas funções que tem muito a ver com ele.

Um dos primeiros problemas que enfrentamos no Javascript com relação ao uso do this, é quando queremos pegar a referência de um método de algum objeto para utilizá-lo em outro lugar como uma função comum. Um exemplo:
var nome = "João"
var idade = 30

var juca = {
  nome: "Juca",
  idade: 40,
  imprime: function () {
    console.log(this.nome + " " + this.idade)
  }
}

function imprime () {
  console.log(this.nome + " " + this.idade)
}

imprime() // João 30

juca.imprime() // Juca 40

Aqui criamos duas variáveis no contexto global, o que seria o mesmo que fazer window.nome ou this.idade, pois o this no contexto global referencia ao próprio objeto window. E criamos as mesmas variáveis dentro do objeto juca também. Dentro de um objeto, o this passa a referenciar a ele mesmo.

O resultado mostrado, portanto, é o que esperávamos inicialmente.

Mas, e se agora nós queremos utilizar o método imprime dentro do objeto juca para utilizá-lo em outro local, que resultado ele mostrará?
var imprime2 = juca.imprime

imprime2() // João 30

E aqui começa nossa primeira confusão. Quando pegamos a referência do método imprime dentro do objeto juca, ele não virá acompanhado do seu próprio this. O this que será utilizado é o da funçao que o invocou, ou seja, do objeto window.

Um outro exemplo que ilustra bem a alteração do this na chamada da função é quando referenciamos o método de um objeto para outro objeto:
var juca = {
  nome: "Juca",
  idade: 40,
  imprime: function () {
    console.log(this.nome + " " + this.idade)
  }
}

var jose = {
  nome: "José",
  idade: 32
}

juca.imprime() // Juca 40

jose.imprime = juca.imprime

jose.imprime() // José 32

Novamente, o this utilizado no método imprimi é do objeto que o invocou.

Bind

Quando queremos manter o this do objeto que definiu aquele método, precisamos usar o bind().
jose.imprime = juca.imprime.bind(juca)

jose.imprime() // Juca 40

O que o bind está fazendo aqui é retornar o mesmo objeto, mas com o this do objeto passado como parâmetro. Como passamos o objeto juca, o método imprimi utilizará o seu this para executar a função.

Outro problema que acontece é quando precisamos passar um método que utiliza o this como um callback para outra função:
function cumprimentos (imprimeNome) {
  console.log("Olá " + imprimeNome())
}

var obj = {
  nome: "José",
  imprimirNome: function () { return this.nome}
}

cumprimentos(obj.imprimirNome) // Olá undefined

Aqui ocorre o mesmo problema, o this que está sendo utilizado pela função imprimeNome é o do objeto window, pois foi quem a invocou. Para corrigir esse comportamento inesperado, basta utilizar o método bind novamente:
//...

cumprimentos(obj.imprimirNome.bind(obj) // Olá José

Com o método bind também é possível criarmos novas funções com alguns parâmetros já definidos por padrão. Uma técnica que vem da programação funcional, chamada currying. Vamos lá!

Suponha que criamos uma função com três parâmetros, mas, em geral, costumamos alterar apenas dois deles com frequência, sendo o primeiro quase sempre o mesmo. Para evitar ter que passar o primeiro argumento a todo momento, podemos fazer com que o bind nos retorne a mesma função, mas com o primeiro parâmetro sempre definido com um valor padrão:
function dadosPessoais (cidade, nome, idade) {
  return nome + " tem " + idade + " anos e nasceu em " + cidade
}

const nascidosEmMinas = dadosPessoais.bind(undefined, "Minas Gerais")

nascidosEmMinas("José", 32) // José tem 32 anos e nasceu em Minas Gerais
nascidosEmMinas("Juca", 40) // Juca tem 40 anos e nasceu em Minas Gerais
nascidosEmMinas("João", 30) // João tem 30 anos e nasceu em Minas Gerais


Call e Apply

Vimos que o bind é o método que podemos utilizar para resolver todos os problemas com o this. Mas, e se quisermos simplesmente invocarmos uma função utilizando o this de outro objeto diretamente, sem ter que referenciá-la para outra função com o bind? Nesse caso podemos utilizar o apply() e o call().

Com esses dois métodos podemos invocar uma função passando o contexto de qualquer objeto que quisermos. Veja um exemplo:
var joao = {
  nome: "João",
  idade: 20,
  cidade: "São Paulo"
}

var juca = {
  nome: "Juca",
  idade: 40,
  cidade: "Rio de Janeiro"
}

function imprimeDados () {
  console.log(this.nome + ", " + this.idade + " anos, " + "nascido em " + this.cidade)
}

imprimeDados()   // undefined, undefined anos, nascido em undefined
imprimeDados.call(joao)  // João, 20 anos, nascido em São Paulo
imprimeDados.apply(juca) // Juca, 40 anos, nascido em Rio de Janeiro

Como pode ser visto no exemplo, a função foi chamada utilizando o this de cada objeto passado como parâmetro.

Até aqui os métodos call e apply possuem a mesma função. O que os diferenciam é a forma como são utilizados quando precisamos invocar funções que recebem parâmetros. Vou alterar a função imprimeDados para que tenhamos que passar os valores de idade e cidade como parâmetro:
var joao = {
  nome: "João"
}

var juca = {
  nome: "Juca",
}

function imprimeDados (idade, cidade) {
  console.log(this.nome + ", " + idade + " anos, " + "nascido em " + cidade)
}

imprimeDados.call(joao, 20, "São Paulo")
imprimeDados.apply(juca, [40, "Rio de Janeiro"])

// ou

var dados = [40, "Rio de Janeiro"]
imprimeDados.apply(juca, dados)

Enquanto os parâmetros no método call vêm normalmente depois do objeto que será utilizado como contexto, no apply, precisamos passá-los como um array. Apesar de não parecer grande coisa inicialmente, dá pra fazer umas coisas bem legais com o apply por causa do seu comportamento.

Se quiséssemos achar os valores máximos e mínimos de um array, por exemplo. Teríamos que fazer um código assim:
var lista = [4,5,2,8,7,3,9,6,10,1]

max = -Infinity, min = +Infinity

for (var i = 0; i < lista.length; i++) {
  if (lista[i] > max) {
    max = lista[i]
  }
  if (lista[i] < min) {
    min = lista[i]
  }
}

Mas, como o apply nos deixa passar um array como substituto de uma cadeia de parâmetros, o algoritmo acima fica bem mais simples:
var lista = [4,5,2,8,7,3,9,6,10,1]
var max = Math.max.apply(undefined, lista) 
var min = Math.min.apply(undefined, lista)

Vale notar que, apesar de utilizar o apply nessa condição faça com que o código fique mais enxuto, é importante não ultrapassar o limite máximo de parâmetros que uma função pode receber. O firefox mesmo, aceita funções de no máximo 65536 parâmetros.

Em resumo, quando ficar em dúvida de qual objeto o this está referenciando em determinado momento, lembre-se: o objeto que invoca uma função é sempre o que está sendo referenciado pelo this naquele momento. E, se a função está sendo chamada de forma pura no código, então o this vai referenciar o objeto window.

Espero que o assunto tenha ficado mais fácil de entender agora. Caso tenha alguma dúvida ou sugestão pro artigo, deixe um comentário para mim no artigo. Assim que possível eu irei responder.

Até mais!