Uma das linguagens de programação mais populares desde o início dos anos 2000 (se não a mais popular), Javascript é considerada uma linguagem orientada a objetos. Para aqueles que não estão familiarizados com o termo, isso significa que em Javascript podemos aplicar naturalmente alguns dos conceitos principais da programação orientada a objetos, que utiliza a declaração de classes e objetos e usa suas propriedades e métodos (funções internas de objetos).
Vamos observar a classe Vehicle como exemplo:
class Vehicle {
name;
numberOfPassengers;
move;
constructor(name, numberOfPassengers, move){
this.name = name;
this.numberOfPassengers = numberOfPassengers;
this.move = move;
}
}
Vemos que um Vehicle tem algumas variáveis definidas: name
, numberOfPassengers
, e move
. Por padrão, todas as classes têm um método construtor, que serve para construir um novo objeto da classe fornecida. O método construtor de nossa classe Vehicle recebe parâmetros que são atribuídos às suas variáveis internas do objeto.
Observe que, para atribuir os parâmetros às variáveis do objeto, o construtor usa a palavra this.
antes do nome das variáveis. Essa palavra-chave this
se refere ao contexto que envolve aquela função, e engloba tudo dentro das chaves de definição da classe.
A maneira de construir um veículo é usando a palavra-chave new
seguido de uma chamada ao método construtor da classe(No nosso caso Vehicle()
) com os argumentos necessários, atribuindo o valor a uma variável, como no exemplo abaixo:
function drive(){
console.log("dirigindo um " + this.name)
}
var car = new Vehicle("Porshe", 2, drive)
console.log(car.name)
console.log(car.numberOfPassengers)
console.log(car.move())
// Saída esperada:
// Porshe
// 2
// dirigindo um Porshe
Podemos ver que nosso novo objeto Vehicle é um Porshe que pode transportar até 2 passageiros. Ele também recebe uma função, drive
, que imprime no console. Podemos acessar as propriedades do carro e imprimi-las no console, assim como podemos chamar seu método move
e o resultado também é impresso no console.
No entanto, vamos considerar a função ride
abaixo. Ela é muito semelhante à função drive
do código anterior.
function ride(){
console.log("passeando de " + this.name)
}
ride()
// Saída esperada:
// passeando de undefined
Se chamarmos a função ride
da forma como está, obtemos a mensagem "passeando de undefined" no console. Isso acontece porque, quando a chamamos, não há uma variável name
nesse contexto para ser referenciada. Ao construir um objeto e passar para ele uma função que usa o contexto envolvente, a função o detecta automaticamente e o utiliza. É assim que nosso objeto plane
abaixo funciona corretamente, assim como o nosso objeto car
anterior.
function fly(){
console.log("pilotando um " + this.name)
}
var plane = new Vehicle("Boeing 777-300", 368, fly)
plane.move()
// Saída esperada:
// pilotando um Boeing 777-300
Essa é uma maneira muito simples de entender como contextos funcionam no Javascript, mas às vezes encontramos situações mais complexas. Por exemplo, digamos que você queira observar alterações em um arquivo e exibir o conteúdo desse arquivo sempre que ele for alterado. Para isso, usamos a função watch
do módulo fs
do Node. Digamos que sua classe FileHandler
seja semelhante à apresentada abaixo:
import { watch, promises } from "fs";
class FileHandler {
watch(event, filename) {
this.showContent(filename);
}
async showContent(filename) {
// o conteúdo é retornado como um buffer, então convertemos para string
const content = await promises.readFile(filename).toString()
console.log(content);
}
}
const fileHandler = new FileHandler()
watch(__filename, fileHandler.watch)
Se tentarmos passar o método fileHandler.watch
como no trecho acima, ele será ignorado pela linguagem. Isso ocorre devido a outro conceito da programação orientada a objetos que é aplicado naturalmente em Javascript, que é a herança ( segundo a herança uma classe pode herdar propriedades e métodos de outra classe, permitindo o reuso de código e relacionamentos hierárquicos entre classes).
Em vez disso, a linguagem tentará usar o método watch
do objeto FSWatcher
, que está dentro do módulo fs
, e possui um método watch
interno. No entanto, esse objeto não possui um método showContent
como nosso fileHandler
, o que resultará em um erro.
Esse é um problema de contexto que pode ser resolvido especificando em qual contexto a função que estamos tentando chamar está. Para isso, temos três métodos herdados por todos os objetos Javascript: bind
, call
e apply
.
Bind
O método bind cria uma cópia de uma função vinculada ao contexto fornecido como primeiro argumento. Se quisermos resolver nosso problema com esse método, podemos fazer da seguinte maneira:
watch(__filename, fileHandler.watch.bind(fileHandler));
Pode parecer redundante à primeira vista, mas dessa forma nos certificamos de que, durante a execução, o método watch
que estamos chamando será aquele contido no objeto fileHandler
. Como o método bind
cria uma cópia da função, também podemos atribuí-lo a uma nova variável passando-a logo em seguida, assim:
var boundFileHandlerWatch = fileHandler.watch.bind(fileHandler)
watch(__filename, boundFileHandlerWatch);
Call
O método call chama a função alterando o contexto para o contexto passado como primeiro argumento. Resolver nosso problema com esse método ficaria assim:
watch(__filename, fileHandler.watch.call(fileHandler));
Apply
O método apply
, de forma semelhante ao método anterior call
, chama a função alterando o contexto para o contexto passado como primeiro argumento.
A única diferença aqui é como passamos os argumentos para o método que estamos alterando o contexto. Enquanto o método call
recebe os argumentos individualmente, o método apply
recebe um array de valores como segundo argumento.
Como exemplo, observe nosso fileHandler
. Seu método watch
recebe dois argumentos: o evento e o nome do arquivo:
class FileHandler {
watch(event, filename) {
this.showContent(filename);
}
async showContent(filename) {
const content = await promises.readFile(filename).toString();
console.log(content);
}
}
Para simular a forma como fs.watch
chama nosso método fileHandler.watch
, podemos alterar o contexto do nosso método para outro contexto onde há um método showContent
e passar os argumentos como nos exemplos abaixo:
var callContext = { showContent: () => console.log("hello call!") };
fileHandler.watch.call(callContext, "change", __filename);
var applyContext = { showContent: () => console.log("hello apply!") };
fileHandler.watch.apply(applyContext, ["change", __filename]);
// Saída esperada:
// hello call!
// hello apply!
Vemos que tanto call
quanto apply
são muito semelhantes. Ao usá-los, basta ter cuidado para passar os argumentos na ordem correta, pois os parâmetros serão recebidos na ordem respectivamente.
É isso aí! Da próxima vez que você passar métodos entre contextos, certifique-se de especificá-los corretamente para evitar problemas como esse.