Ao passar métodos de objeto como callbacks, por exemplo para setTimeout
, há um problema conhecido: “perder this
”.
Neste capítulo, veremos as maneiras de consertar isso.
Já vimos exemplos de perda this
. Uma vez que um método é passado em algum lugar separado do objeto – this
é perdido.
Veja como isso pode acontecer com setTimeout
:
deixe usuário = { primeiroNome: "João", digaOi() { alert(`Olá, ${this.firstName}!`); } }; setTimeout(user.sayHi, 1000); // Olá, indefinido!
Como podemos ver, a saída não mostra “John” como this.firstName
, mas undefined
!
Isso porque setTimeout
obteve a função user.sayHi
, separadamente do objeto. A última linha pode ser reescrita como:
deixe f = usuário.sayHi; setTimeout(f, 1000); //perdeu o contexto do usuário
O método setTimeout
no navegador é um pouco especial: ele define this=window
para a chamada de função (para Node.js, this
se torna o objeto timer, mas realmente não importa aqui). Então, para this.firstName
ele tenta obter window.firstName
, que não existe. Em outros casos semelhantes, geralmente this
se torna undefined
.
A tarefa é bastante típica – queremos passar um método de objeto para algum outro lugar (aqui – para o escalonador) onde ele será chamado. Como ter certeza de que será chamado no contexto certo?
A solução mais simples é usar uma função de empacotamento:
deixe usuário = { primeiroNome: "João", digaOi() { alert(`Olá, ${this.firstName}!`); } }; setTimeout(função(){ user.sayHi(); // Olá, João! }, 1000);
Agora funciona, pois recebe user
do ambiente lexical externo e depois chama o método normalmente.
O mesmo, mas mais curto:
setTimeout(() => user.sayHi(), 1000); // Olá, João!
Parece bom, mas uma pequena vulnerabilidade aparece na nossa estrutura de código.
E se antes setTimeout
disparar (há um segundo de atraso!) user
alterar o valor? Então, de repente, ele chamará o objeto errado!
deixe usuário = { primeiroNome: "João", digaOi() { alert(`Olá, ${this.firstName}!`); } }; setTimeout(() => user.sayHi(), 1000); // ...o valor do usuário muda em 1 segundo usuário = { sayHi() { alert("Outro usuário em setTimeout!"); } }; // Outro usuário em setTimeout!
A próxima solução garante que tal coisa não acontecerá.
As funções fornecem um método de ligação integrado que permite corrigir this
.
A sintaxe básica é:
// uma sintaxe mais complexa virá um pouco mais tarde deixe vinculadoFunc = func.bind(context);
O resultado de func.bind(context)
é um “objeto exótico” semelhante a uma função especial, que pode ser chamado como função e passa de forma transparente a chamada para func
configurando this=context
.
Em outras palavras, boundFunc
é como func
com consertado this
.
Por exemplo, aqui funcUser
passa uma chamada para func
com this=user
:
deixe usuário = { primeiroNome: "João" }; função função() { alerta(este.primeiroNome); } deixe funcUser = func.bind(usuário); funcUser(); // John
Aqui func.bind(user)
como uma “variante vinculada” de func
, com this=user
corrigido.
Todos os argumentos são passados para a func
original “como estão”, por exemplo:
deixe usuário = { primeiroNome: "João" }; função func(frase) { alerta(frase + ', ' + this.firstName); } // vincula isso ao usuário deixe funcUser = func.bind(usuário); funcUser("Olá"); // Olá, John (o argumento "Hello" é passado e this=user)
Agora vamos tentar com um método de objeto:
deixe usuário = { primeiroNome: "João", digaOi() { alert(`Olá, ${this.firstName}!`); } }; deixe dizerOi = usuário.sayHi.bind(usuário); // (*) //pode executá-lo sem um objeto digaOi(); // Olá, João! setTimeout(digaOi, 1000); // Olá, João! // mesmo que o valor do usuário mude dentro de 1 segundo // sayHi usa o valor pré-vinculado que é uma referência ao antigo objeto de usuário usuário = { sayHi() { alert("Outro usuário em setTimeout!"); } };
Na linha (*)
pegamos o método user.sayHi
e o vinculamos a user
. sayHi
é uma função “vinculada”, que pode ser chamada sozinha ou passada para setTimeout
– não importa, o contexto estará correto.
Aqui podemos ver que os argumentos são passados “como estão”, só que this
é corrigido por bind
:
deixe usuário = { primeiroNome: "João", dizer(frase) { alerta(`${frase}, ${this.firstName}!`); } }; digamos = user.say.bind (usuário); diga("Olá"); // Olá, João! (O argumento "Olá" é passado para dizer) diga("Tchau"); // Tchau, João! ("Tchau" é passado para dizer)
Método de conveniência: bindAll
Se um objeto tiver muitos métodos e planejamos distribuí-lo ativamente, poderíamos vincular todos eles em um loop:
for (deixe digitar o usuário) { if (typeof usuário[chave] == 'função') { usuário[chave] = usuário[chave].bind(usuário); } }
As bibliotecas JavaScript também fornecem funções para vinculação em massa conveniente, por exemplo, _.bindAll(object, methodNames) em lodash.
Até agora falamos apenas em vincular this
. Vamos dar um passo adiante.
Podemos vincular não apenas this
, mas também argumentos. Isso raramente é feito, mas às vezes pode ser útil.
A sintaxe completa de bind
:
deixe vinculado = func.bind(contexto, [arg1], [arg2], ...);
Permite vincular o contexto como this
e os argumentos iniciais da função.
Por exemplo, temos uma função de multiplicação mul(a, b)
:
função mul(a, b) { retornar a*b; }
Vamos usar bind
para criar uma função double
em sua base:
função mul(a, b) { retornar a*b; } deixe double = mul.bind(null, 2); alerta(duplo(3) ); // = mul(2, 3) = 6 alerta(duplo(4) ); // = mul(2, 4) = 8 alerta(duplo(5) ); // = mul(2, 5) = 10
A chamada para mul.bind(null, 2)
cria uma nova função double
que passa chamadas para mul
, fixando null
como o contexto e 2
como o primeiro argumento. Outros argumentos são passados “como estão”.
Isso é chamado de aplicação de função parcial – criamos uma nova função fixando alguns parâmetros da função existente.
Observe que na verdade não usamos this
aqui. Mas bind
exige isso, então devemos colocar algo como null
.
A função triple
no código abaixo triplica o valor:
função mul(a, b) { retornar a*b; } deixe triplo = mul.bind(null, 3); alerta(triplo(3) ); // = mul(3, 3) = 9 alerta(triplo(4) ); // = mul(3, 4) = 12 alerta(triplo(5) ); // = mul(3, 5) = 15
Por que geralmente fazemos uma função parcial?
A vantagem é que podemos criar uma função independente com um nome legível ( double
, triple
). Podemos usá-lo e não fornecer o primeiro argumento todas as vezes, pois é corrigido com bind
.
Em outros casos, a aplicação parcial é útil quando temos uma função muito genérica e queremos uma variante menos universal dela por conveniência.
Por exemplo, temos uma função send(from, to, text)
. Então, dentro de um objeto user
, podemos querer usar uma variante parcial dele: sendTo(to, text)
que envia do usuário atual.
E se quisermos corrigir alguns argumentos, mas não o contexto this
? Por exemplo, para um método de objeto.
A bind
nativa não permite isso. Não podemos simplesmente omitir o contexto e saltar para os argumentos.
Felizmente, uma função partial
para vincular apenas argumentos pode ser facilmente implementada.
Assim:
função parcial(func, ...argsBound) { retornar função(...args) { // (*) retornar func.call(this, ...argsBound, ...args); } } // Uso: deixe usuário = { primeiroNome: "João", diga(hora, frase) { alert(`[${time}] ${this.firstName}: ${frase}!`); } }; //adiciona um método parcial com tempo fixo user.sayNow = parcial(user.say, new Date().getHours() + ':' + new Date().getMinutes()); user.sayNow("Olá"); //Algo como: // [10:00] João: Olá!
O resultado da chamada partial(func[, arg1, arg2...])
é um wrapper (*)
que chama func
com:
this
mesmo que acontece (para user.sayNow
chame seu user
)
Em seguida, fornece ...argsBound
– argumentos da chamada partial
( "10:00"
)
Em seguida, fornece ...args
– argumentos dados ao wrapper ( "Hello"
)
É tão fácil fazer isso com a sintaxe de propagação, certo?
Também há uma implementação _.partial pronta da biblioteca lodash.
O método func.bind(context, ...args)
retorna uma “variante vinculada” da função func
que corrige o contexto this
e os primeiros argumentos, se fornecidos.
Normalmente aplicamos bind
para corrigir this
para um método de objeto, para que possamos passá-lo para algum lugar. Por exemplo, para setTimeout
.
Quando fixamos alguns argumentos de uma função existente, a função resultante (menos universal) é chamada parcialmente aplicada ou parcial .
Parciais são convenientes quando não queremos repetir o mesmo argumento indefinidamente. Por exemplo, se tivermos uma função send(from, to)
e from
deve ser sempre a mesma para nossa tarefa, podemos obter uma parcial e continuar com ela.
importância: 5
Qual será o resultado?
função f() { alerta(isto); // ? } deixe usuário = { g: f.bind(nulo) }; usuário.g();
A resposta: null
.
função f() { alerta(isto); // nulo } deixe usuário = { g: f.bind(nulo) }; usuário.g();
O contexto de uma função vinculada é fixo. Simplesmente não há como mudar isso ainda mais.
Portanto, mesmo enquanto executamos user.g()
, a função original é chamada com this=null
.
importância: 5
Podemos mudar this
por meio de vinculação adicional?
Qual será o resultado?
função f() { alerta(este.nome); } f = f.bind( {nome: "John"} ).bind( {nome: "Ann" } ); f();
A resposta: João .
função f() { alerta(este.nome); } f = f.bind( {nome: "John"} ).bind( {nome: "Pete"} ); f(); // John
O objeto de função vinculado exótico retornado por f.bind(...)
lembra o contexto (e os argumentos, se fornecidos) apenas no momento da criação.
Uma função não pode ser vinculada novamente.
importância: 5
Existe um valor na propriedade de uma função. Isso mudará após bind
? Por que ou por que não?
function digaOi() { alerta(este.nome); } digaOi.teste = 5; deixe vinculado = sayHi.bind({ nome: "João" }); alerta(limitado.teste); // qual será a saída? por que?
A resposta: undefined
.
O resultado da bind
é outro objeto. Não possui a propriedade test
.
importância: 5
A chamada para askPassword()
no código abaixo deve verificar a senha e então chamar user.loginOk/loginFail
dependendo da resposta.
Mas isso leva a um erro. Por que?
Corrija a linha destacada para que tudo comece a funcionar corretamente (as demais linhas não devem ser alteradas).
function perguntarSenha(ok, falha) { deixe senha = prompt("Senha?", ''); if (senha == "rockstar") ok(); senão falha(); } deixe usuário = { nome: 'João', loginOk() { alert(`${this.name} logado`); }, loginFail() { alert(`${this.name} falhou ao fazer login`); }, }; perguntarPassword(user.loginOk, user.loginFail);
O erro ocorre porque askPassword
obtém funções loginOk/loginFail
sem o objeto.
Quando os chama, eles naturalmente assumem this=undefined
.
Vamos bind
o contexto:
function perguntarSenha(ok, falha) { deixe senha = prompt("Senha?", ''); if (senha == "rockstar") ok(); senão falha(); } deixe usuário = { nome: 'João', loginOk() { alert(`${this.name} logado`); }, loginFail() { alert(`${this.name} falhou ao fazer login`); }, }; askPassword(user.loginOk.bind(usuário), user.loginFail.bind(usuário));
Agora funciona.
Uma solução alternativa poderia ser:
//... askPassword(() => user.loginOk(), () => user.loginFail());
Geralmente isso também funciona e parece bom.
É um pouco menos confiável em situações mais complexas em que a variável user
pode mudar após a chamada askPassword
, mas antes que o visitante responda e chame () => user.loginOk()
.
importância: 5
A tarefa é uma variante um pouco mais complexa de Corrigir uma função que perde "isso".
O objeto user
foi modificado. Agora, em vez de duas funções loginOk/loginFail
, ele tem uma única função user.login(true/false)
.
O que devemos passar askPassword
no código abaixo, para que ele chame user.login(true)
como ok
e user.login(false)
como fail
?
function perguntarSenha(ok, falha) { deixe senha = prompt("Senha?", ''); if (senha == "rockstar") ok(); senão falha(); } deixe usuário = { nome: 'João', login(resultado) { alert(this.name + (resultado? 'logado': 'falha ao fazer login')); } }; pergunteSenha(?, ?); // ?
Suas alterações devem modificar apenas o fragmento destacado.
Use uma função wrapper, uma seta para ser conciso:
askPassword(() => user.login(true), () => user.login(false));
Agora ele obtém user
de variáveis externas e o executa da maneira normal.
Ou crie uma função parcial de user.login
que use user
como contexto e tenha o primeiro argumento correto:
askPassword(user.login.bind(usuário, verdadeiro), user.login.bind(usuário, falso));