Decoradores e Encaminhamento em JavaScript: call e apply
Aprenda a escrever funções decoradoras em JavaScript e encaminhar chamadas com func.call e func.apply, incluindo decorador de cache, bind e empréstimo de métodos.
Um decorador é uma função invólucro: recebe outra função e retorna uma nova função que adiciona comportamento — registro, cache, temporização, verificações de acesso — ao redor da original, sem modificar o código dela. Para construir decoradores que funcionem com qualquer função, é necessário uma forma confiável de chamar uma função com um this escolhido e um conjunto de argumentos escolhido. É exatamente isso que func.call e func.apply fornecem.
Este capítulo abrange funções decoradoras (invólucros), encaminhamento de this e argumentos com call/apply, restauração de contexto perdido com bind, e empréstimo de métodos.
Nota: Isto trata de decoradores de função — o padrão cotidiano disponível em JavaScript puro hoje em dia. Os mais recentes decoradores de classe com prefixo
@são um recurso separado e mais avançado (atualmente uma proposta Stage 3 que requer um transpilador), e não são abordados aqui.
O que é um decorador
Um decorador é uma função que envolve uma função-alvo e retorna um substituto com comportamento extra. Como o invólucro tem a mesma forma externa, os chamadores não precisam mudar.
function sum(a, b) {
return a + b;
}
function logged(func) {
return function (a, b) {
console.log(`calling with ${a}, ${b}`);
return func(a, b);
};
}
const loggedSum = logged(sum);
console.log(loggedSum(2, 3));
// calling with 2, 3
// 5O invólucro é reutilizável, mantém o original intacto e pode ser empilhado. O problema acima é que ele só lida com uma função que recebe exatamente dois argumentos e nenhum this. Para envolver qualquer função, encaminhamos a chamada.
Um decorador de cache
Um decorador comum no mundo real armazena em cache os resultados para que uma função custosa execute apenas uma vez por entrada. Experimente:
Isso funciona para uma função independente. Mas no momento em que slow é um método que usa this, chamar func(x) o quebra — o invólucro perde o contexto do objeto. É aí que call e apply entram.
Encaminhando a chamada: call e apply
call e apply ambos invocam uma função com um this explicitamente escolhido. Eles diferem apenas na forma como os argumentos são passados:
func.call(thisArg, arg1, arg2, ...)— argumentos listados individualmente.func.apply(thisArg, argsArray)— argumentos como um único array (ou similar a array).
call
apply
Estas duas chamadas são equivalentes:
func.call(obj, 1, 2, 3);
func.apply(obj, [1, 2, 3]);Use call quando você conhece os argumentos individualmente; use apply quando já os tem em um array. Com a sintaxe spread (func.call(obj, ...args)) a distinção frequentemente desaparece — veja Parâmetros rest e sintaxe spread.
Encaminhando this com call
Agora podemos corrigir o decorador de cache para métodos. Dentro do invólucro, this é o objeto no qual o método foi chamado, então o encaminhamos com func.call(this, x):
Sem func.call(this, x), a chamada interna seria func(x) e this seria perdido, fazendo this.someMethod() falhar.
Encaminhando todos os argumentos com apply
Para um método com vários argumentos, encaminhe todos os argumentos de uma vez. O invólucro não sabe quantos há, então os lê de arguments e os passa todos através de func.apply(this, arguments):
Passar this e arguments diretamente é chamado de encaminhamento de chamada: o invólucro se comporta exatamente como o original, apenas com lógica extra ao redor.
Empréstimo de métodos
A função hash acima usa um truque. arguments é similar a array (tem índices e length) mas não é um array real, portanto não tem join. Em vez de converter, nós emprestamos o método de array:
function hash(args) {
return [].join.call(args, ',');
}
console.log(hash([3, 5])); // "3,5"[].join é Array.prototype.join. Chamá-lo com args como this executa a lógica de join sobre o valor similar a array. O empréstimo de métodos permite reutilizar métodos nativos em objetos que não são daquele tipo.
bind e contexto perdido
call e apply invocam imediatamente. bind em vez disso retorna uma nova função com this permanentemente fixado — útil quando a chamada acontece depois (um callback, um manipulador de eventos, um setTimeout).
O problema que bind resolve é a perda de contexto: desanexe um método de seu objeto e this não aponta mais para ele.
Para uma análise mais aprofundada sobre como corrigir contexto em callbacks e a diferença entre bind, arrow functions e call/apply, veja Vinculação de funções.
Quando usar qual
| Objetivo | Use |
|---|---|
Chamar agora com this escolhido, args listados individualmente | func.call(thisArg, a, b) |
Chamar agora com this escolhido, args já em um array | func.apply(thisArg, args) |
Obter uma função para chamar depois com this fixado | func.bind(thisArg) |
| Reutilizar um método nativo em um objeto similar a array | emprestá-lo: [].method.call(obj, …) |
Conclusão
Decoradores envolvem uma função para adicionar comportamento sem alterá-la. Para fazer um invólucro funcionar com qualquer função — incluindo métodos — encaminhe a chamada original com func.call(this, ...) ou func.apply(this, arguments), use bind quando a chamada for adiada, e empreste métodos nativos quando um objeto for apenas similar a array. Juntos, esses recursos fornecem abstrações reutilizáveis e seguras quanto ao contexto, como o decorador de cache acima.
Leitura relacionada: Métodos de object, "this", Objeto de função, NFE, e Vinculação de funções.