07 de jan de 2020 - 10 min de leitura
Entendendo This, Bind, Call e Apply no JavaScript
Assimilando os conceitos
Introdução
Um conceito muito importante para quem trabalha com JavaScript é o uso da palavra-chave this, porém o seu uso pode ser um pouco confuso para alguns desenvolvedores, principalmente quando vindo de outras linguagens.
De forma resumida o this
referencia um objeto, e seu valor pode variar dependendo se o objeto é global, se está sob o modo estrito ou em um construtor. E também pode variar de forma explícita com base na utilização dos function proptotype methods bind, call e apply.
Complexo, né? Porém nesse artigo vamos desvendar o uso do this
em seus diferentes contextos.
Contexto implícito
Existem 4 contextos principais onde o this
pode ser inferido implicitamente:
- Contexto global
- Como um método dentro de um objeto
- Como o construtor de uma função ou classe
- Como manipulador de eventos no DOM
Global
No contexto global this
tem como referência o Global object.
Quando estamos trabalhando no navegador, o contexto global é o window
. Quando estamos trabalhando no node, o contexto global é o global
.
Para os exemplos vamos utilizar console do Google Chrome. Para isso, basta abrir o navegador, apertar f12 e selecionar a aba console.
Feito isso, vamos ao código.
No console do navegador:
console.log(this);
// Output
Window {parent: Window, opener: null, top: Window, length: 0, frames: Window, …}
Vemos que o valor de this
é Window
. Isso porque Window
é o objeto global do Browser.
Sabemos que funções possuem seu próprio contexto/escopo para variáveis. Dito isso, podemos pensar que se colocarmos o this
dentro de uma função, o this
fará referência a função a qual seu contexto está inserido, mas não. Uma função de nível superior ainda manterá a referência do this
como sendo o objeto global.
Podemos escrever uma função de nível superior ou uma função que não está associada a nenhum objeto, como esta:
function printThis() {
console.log(this);
}
printThis();
// Output
Window {parent: Window, opener: null, top: Window, length: 0, frames: Window, …}
Vemos que mesmo inserido no contexto da função, o this
faz referência ao objeto global Window
.
No entanto, ao usarmos o modo estrito do JavaScript,
o valor de this
dentro de uma função será undefined
.
'use strict'
function printThis() {
console.log(this);
}
printThis();
// Output
undefined
Geralmente, é mais seguro usar o modo estrito para reduzir a probabilidade do this
ter um escopo inesperado. Raramente alguém vai querer se referir ao window
usando this
.
Para obter mais informações sobre o modo estrito você pode consultar a documentação do MDN.
Um método de objeto
Um método é basicamente uma função armazenada em um objeto. Uma ação que esse objeto pode executar. Um método usa this
para se referenciar as propriedades de um objeto.
const people = {
name: 'Maicon Silva',
yearOfBirth: 1995,
describe() {
console.log(`${this.name} was born in ${this.yearOfBirth}`);
},
};
people.describe();
// Output
"Maicon Silva was born in 1995"
Neste exemplo this
é o mesmo que people.
Em um objeto aninhado, this
refere-se ao escopo do objeto atual do método. No exemplo a seguir, this.age
dentro do details
se refere a people.details.age
.
const people = {
name: 'Maicon Silva',
yearFounded: 1995,
details: {
age: 24,
printDetails() {
console.log(`The age is ${this.age}`);
}
},
};
people.details.printDetails();
// Output
"The age is 24"
Outra maneira de se pensar sobre isso é que this
tem como referência o primeiro objeto no lado esquerdo do método.
Um construtor de funções
Quando usamos a palavra-chave new
, o JS cria uma instância de uma função ou classe. Os construtores de funções eram a maneira padrão de inicializar um objeto definido pelo programador antes da introdução de classes do ECMAScript 2015.
function Country(name, yearFounded) {
this.name = name;
this.yearFounded = yearFounded;
this.describe = function() {
console.log(`${this.name} was founded in ${this.yearFounded}`);
};
}
const brasil = new Country('Braisl', 1822);
brasil.describe();
// Output
"Brasil was founded in 1822"
Nesse contexto, this
está vinculado à instancia de Country
, que está contida na constante brasil
.
Um construtor de classe
Um construtor de classe se assemelha a um construtor de funções, criando uma instância da classe e armazenando em uma variável. Existem diferenças fundamentais entre funções construtoras e classes que abordaremos num próximo post, mas podemos ver como o this
se comporta em um construtor de classe:
class Country {
constructor(name, yearFounded) {
this.name = name;
this.yearFounded = yearFounded;
}
describe() {
console.log(`${this.name} was founded in ${this.yearFounded}`);
}
}
const brasil = new Country('Braisl', 1822);
brasil.describe();
// Output
"Brasil was founded in 1822"
Da mesma forma que na função construtora, o this
está vinculado à instancia de Country
armazenada na constante brasil.
Um manipulador de eventos no DOM
No navegador, há um contexto do this
especial para manipuladores de eventos. Em um manipulador de eventos como o addEventListner
, this
fará referência a event.currentTarget
. Na maioria das vezes os desenvolvedores simplesmente usam event.target
ou event.currentTarget
conforme o necessário para acessar os eventos do DOM, mas é importante termos em mente como o this
é afetado nesse contexto.
No exemplo a seguir, criaremos um botão, adicionaremos um texto e vamos anexa-lo ao DOM. Quando registrarmos o valor this
dentro do manipulador de evento, ele fará referência ao elemento criado.
const button = document.createElement('button');
button.textContent = 'Click me';
document.body.append(button);
button.addEventListener('click', function(event) {
console.log(this);
});
// Output
<button>Click me</button>
Ao fazer isso, temos um botão anexado à página que diz "Click me". Se clicarmos no botão, veremos <button>Click me</button>
no console, isso porque o this
anexado ao eventListner no botão faz referência ao próprio elemento criado, o button.
Contexto explicito
Em todos os exemplos anteriores, o valor de this
foi determinado por seu contexto implícito - seja global, em um objeto, em uma função construtura ou classe e até mesmo em manipuladores de eventos do DOM. No entanto, usando call
, bind
ou apply
, podemos determinar explícitamente o que this
deve referenciar.
Call e Apply
call
e apply
são muito semelhantes - eles invocam uma função com um contexto de this
especifico e argumentos opcionais. A única diferença entre eles é que call
requer que os argumentos adicionais sejam passados um por um, enquanto apply
recebe os argumentos como uma matriz.
No exemplo a seguir, criaremos um objeto e uma função que faz referência ao this
sem ter o this
referenciado em seu contexto.
const book = {
title: 'The Lord of the rings',
author: 'J.R.R Tolkien',
};
function summary() {
console.log(`${this.title} was written by ${this.author}.`);
}
summary();
// Output
"undefined was written by undefined"
Como a função summary
e o objeto book
não possuem nenhuma ligação, summary
vai imprimir undefined
, pois procura o this.title
e this.author
no objeto global e não os encontra.
Nota: Tentar isso no modo estrito resultaria em
Uncaught TypeError: Cannot read property 'title' of undefined
.
Nesse caso, podemos usar call
ou apply
e chamar o contexto de this
do objeto book
na função.
summary.call(book);
// ou
summary.apply(book);
// Output
"The Lord of the rings was written by J.R.R Tolkien."
Agora existe uma conexão entre book
e summary
, quando esses métodos são aplicados.
Vamos confirmar o que this
está referenciando:
function printThis() {
console.log(this);
}
printThis.call(book);
{title: "The Lord of the rings", author: "J.R.R Tolkien"}
Podemos ver que this
agora referencia o objeto passado como argumento.
É assim que call
e apply
funcionam da mesma maneira, mas como eu disse anteriormente existe uma pequena diferença em como esses métodos recebem argumentos adicionais, onde call
recebe os argumentos um a um, enquanto apply
recebe uma matriz de argumentos.
function longerSummary(genre, year) {
console.log(
`${this.title} was written by ${this.author}. It's a ${genre} history written in the year ${year}.`
);
}
Com o call
cada valor que desejamos enviar para a função é enviado como um argumento adicional:
longerSummary.call(book, 'fantasy', 1937);
// Output
"The Lord of the rings was written by J.R.R Tolkien. It's a fantasia history written in the year 1937."
Se tentarmos enviar os argumentos da mesma forma utilizando o apply
vamos ter uma Exception.
longerSummary.apply(book, 'fantasy', 1937);
// Output
"Uncaught TypeError: CreateListFromArrayLike called on non-object at <anonymous>:1:15"
Diferente do call
, os argumentos adicionais utilizando o apply
devem ser passados em uma matriz:
longerSummary.call(book, ['fantasy', 1937]);
// Output
"The Lord of the rings was written by J.R.R Tolkien. It's a fantasia history written in the year 1937."
A diferença entre passar os parâmetros um a um e em uma matriz é minima, porém é bom estarmos cientes disso. Utilizando o apply
podemos usar um array literal, por exemplo, fun.apply(this, ['comer', 'beber'])
, ou um objeto array
, por exemplo, fun.apply(this, new Array('comer', 'beber'))
.
Bind
Ambos call
e apply
são méotods one-time, ou seja, ao aplicarmos algum contexto em uma função, essa função será executada com contexto uma vez, porém continuará inalterada(sem contexto).
Em alguns casos, pode ser necessário usar um método/função repetidamente com o contexto do this
de outro objeto. Nesse caso podemos usar o bind
para criar um novo método/função com o this
explicito.
const bookWithSummary = summary.bind(book);
bookWithSummary();
// Output
"The Lord of the rings was written by J.R.R Tolkien."
Neste exemplo, quando a função bookWithSummary
for chamada, ela sempre retornará o valor original do this
associado pelo método bind
. A tentativa de vincular um novo contexto à função bookWithSummary
vai falhar, portanto, podemos confiar em uma função ou método associado pelo bind
para retornar o this
esperado.
const bookWithSummary = summary.bind(book);
bookWithSummary(); // "The Lord of the rings was written by J.R.R Tolkien."
const book2 = {
title: 'This is book 2',
author: 'This is author of book 2',
};
bookWithSummary.bind(book2); // "The Lord of the rings was written by J.R.R Tolkien."
No exemplo acima vemos um caso onde bookWithSummary
recebe o segundo bind(book2)
, mas mantém o valor do primeiro.
Arrow Functions
Arrow Functions não possuem um escopo
para o this
. Em vez disso, elas passam para um próximo nível de execução.
const whoAmI = {
name: 'Maicon Silva',
regularFunction: function() {
console.log(this.name);
},
arrowFunction: () => {
console.log(this.name);
}
}
whoAmI.regularFunction(); // "Maicon Silva"
whoAmI.arrowFunction(); // undefined
Pode ser útil usar arrow functions nos casos em que precisamos que o this
faça refrência ao contexto externo. Por exemplo,
caso tivessemos um listener dentro de uma classe, provavelmente o this
precisaria fazer referência a algum valor da classe.
No exemplo a seguir, criaremos um botão no DOM, e também uma classe que terá um listener que vai alterar o valor do botão para um valor referenciado na própria classe:
const button = document.createElement('button');
button.textContent = 'Click me';
document.body.append(button);
class Display {
constructor() {
this.buttonText = 'New Text';
button.addEventListener('click', event => {
event.target.textContent = this.buttonText
});
}
}
new Display();
Quando o botão é acionado seu texto muda para "New Text". Se não tivessemos usado uma arrow function ali o valor
do this
no listener seria o event.currentTarget
e não seriamos capaz de acessar o valor dentro da classe sem vinculá-la
explícitamente à função.
Conclusão
Neste artigo vimos os diferentes valores que o this
pode receber de forma implícita em tempo de execução e explícita utilizando
os métodos call
, bind
e apply
. Também aprendemos como a falta do contexto do this
pode ser usado nas arrow functions.
Dicas, sugestões, conselhos ou melhorias? Você pode entrar em contato comigo pelas minhas redes ou abrir uma PR no repositório aqui.