Al pasar métodos de objeto como devoluciones de llamada, por ejemplo a setTimeout
, existe un problema conocido: "perder this
".
En este capítulo veremos las formas de solucionarlo.
Ya hemos visto ejemplos de pérdida this
. Una vez que un método se pasa a algún lugar separado del objeto, this
se pierde.
Así es como puede suceder con setTimeout
:
dejar usuario = { nombre: "Juan", decir Hola() { alert(`¡Hola, ${this.firstName}!`); } }; setTimeout(usuario.decirHola, 1000); // ¡Hola, indefinido!
Como podemos ver, el resultado no muestra "John" como this.firstName
, sino undefined
.
Esto se debe a que setTimeout
obtuvo la función user.sayHi
, por separado del objeto. La última línea se puede reescribir como:
let f = usuario.sayHola; setTimeout(f,1000); // contexto de usuario perdido
El método setTimeout
en el navegador es un poco especial: establece this=window
para la llamada a la función (para Node.js, this
se convierte en el objeto del temporizador, pero realmente no importa aquí). Entonces, para this.firstName
intenta obtener window.firstName
, que no existe. En otros casos similares, normalmente this
simplemente se vuelve undefined
.
La tarea es bastante típica: queremos pasar un método de objeto a otro lugar (aquí, al programador) donde será llamado. ¿Cómo asegurarse de que se llame en el contexto correcto?
La solución más sencilla es utilizar una función de envoltura:
dejar usuario = { nombre: "Juan", decir Hola() { alert(`¡Hola, ${this.firstName}!`); } }; setTimeout(función() { usuario.sayHola(); // ¡Hola, Juan! }, 1000);
Ahora funciona porque recibe user
del entorno léxico externo y luego llama al método normalmente.
Lo mismo, pero más corto:
setTimeout(() => usuario.sayHi(), 1000); // ¡Hola, Juan!
Se ve bien, pero aparece una ligera vulnerabilidad en nuestra estructura de código.
¿Qué pasa si antes de que se active setTimeout
(¡hay un segundo de retraso!) user
cambia el valor? Entonces, de repente, ¡llamará al objeto equivocado!
dejar usuario = { nombre: "Juan", decir Hola() { alert(`¡Hola, ${this.firstName}!`); } }; setTimeout(() => usuario.sayHi(), 1000); // ...el valor del usuario cambia en 1 segundo usuario = { sayHi() { alert("¡Otro usuario en setTimeout!"); } }; // ¡Otro usuario en setTimeout!
La siguiente solución garantiza que tal cosa no suceda.
Las funciones proporcionan un enlace de método integrado que permite solucionar this
.
La sintaxis básica es:
// una sintaxis más compleja vendrá un poco más tarde let enlazadoFunc = func.bind(contexto);
El resultado de func.bind(context)
es un “objeto exótico” similar a una función especial, que se puede llamar como función y pasa de forma transparente la llamada a func
configurando this=context
.
En otras palabras, llamar boundFunc
es como func
con fix this
.
Por ejemplo, aquí funcUser
pasa una llamada a func
con this=user
:
dejar usuario = { nombre: "Juan" }; función función() { alerta(este.primerNombre); } let funcUser = func.bind(usuario); funcUsuario(); // John
Aquí func.bind(user)
como una "variante vinculada" de func
, con this=user
corregido.
Todos los argumentos se pasan a la func
original "tal cual", por ejemplo:
dejar usuario = { nombre: "Juan" }; función func(frase) { alerta(frase + ', ' + this.firstName); } // vincula esto al usuario let funcUser = func.bind(usuario); funcUsuario("Hola"); // Hola, John (se pasa el argumento "Hola" y this=usuario)
Ahora probemos con un método de objeto:
dejar usuario = { nombre: "Juan", decir Hola() { alert(`¡Hola, ${this.firstName}!`); } }; let decirHola = usuario.decirHola.bind(usuario); // (*) // puedo ejecutarlo sin un objeto decir Hola(); // ¡Hola, Juan! setTimeout(di Hola, 1000); // ¡Hola, Juan! // incluso si el valor del usuario cambia en 1 segundo // sayHi usa el valor pre-enlazado que es una referencia al antiguo objeto de usuario usuario = { sayHi() { alert("¡Otro usuario en setTimeout!"); } };
En la línea (*)
tomamos el método user.sayHi
y lo vinculamos al user
. sayHi
es una función "vinculada", que puede llamarse sola o pasarse a setTimeout
; no importa, el contexto será el correcto.
Aquí podemos ver que los argumentos se pasan "tal cual", solo this
se soluciona mediante bind
:
dejar usuario = { nombre: "Juan", decir (frase) { alert(`${frase}, ${this.firstName}!`); } }; digamos = usuario.say.bind(usuario); decir("Hola"); // ¡Hola, Juan! (El argumento "Hola" se pasa para decir) decir("Adiós"); // ¡Adiós, Juan! ("Adiós" se pasa a decir)
Método de conveniencia: bindAll
Si un objeto tiene muchos métodos y planeamos pasarlo activamente, entonces podríamos vincularlos todos en un bucle:
para (dejar ingresar usuario) { if (tipo de usuario [clave] == 'función') { usuario[clave] = usuario[clave].bind(usuario); } }
Las bibliotecas de JavaScript también proporcionan funciones para un enlace masivo conveniente, por ejemplo, _.bindAll(object, MethodNames) en lodash.
Hasta ahora sólo hemos hablado de vincular this
. Vayamos un paso más allá.
Podemos vincular no solo this
, sino también argumentos. Esto rara vez se hace, pero a veces puede resultar útil.
La sintaxis completa de bind
:
let enlazado = func.bind(contexto, [arg1], [arg2], ...);
Permite vincular el contexto como this
y los argumentos iniciales de la función.
Por ejemplo, tenemos una función de multiplicación mul(a, b)
:
función mul(a, b) { devolver a * b; }
Usemos bind
para crear una función double
en su base:
función mul(a, b) { devolver a * b; } let doble = mul.bind(nulo, 2); alerta (doble(3)); // = mul(2, 3) = 6 alerta (doble(4)); // = mul(2, 4) = 8 alerta (doble(5)); // = mul(2, 5) = 10
La llamada a mul.bind(null, 2)
crea una nueva función double
que pasa llamadas a mul
, fijando null
como contexto y 2
como primer argumento. Los argumentos adicionales se pasan "tal cual".
Esto se llama aplicación de función parcial: creamos una nueva función fijando algunos parámetros de la existente.
Tenga en cuenta que en realidad no usamos this
aquí. Pero bind
lo requiere, por lo que debemos poner algo como null
.
La función triple
en el siguiente código triplica el valor:
función mul(a, b) { devolver a * b; } let triple = mul.bind(null, 3); alerta( triple(3) ); // = mul(3, 3) = 9 alerta( triple(4) ); // = mul(3, 4) = 12 alerta( triple(5) ); // = mul(3, 5) = 15
¿Por qué solemos hacer una función parcial?
El beneficio es que podemos crear una función independiente con un nombre legible ( double
, triple
). Podemos usarlo y no proporcionar el primer argumento cada vez, ya que se soluciona con bind
.
En otros casos, la aplicación parcial es útil cuando tenemos una función muy genérica y queremos una variante menos universal de la misma por conveniencia.
Por ejemplo, tenemos una función send(from, to, text)
. Luego, dentro de un objeto user
es posible que queramos usar una variante parcial del mismo: sendTo(to, text)
que envía desde el usuario actual.
¿Qué pasa si quisiéramos corregir algunos argumentos, pero no el this
? Por ejemplo, para un método de objeto.
El bind
nativo no lo permite. No podemos simplemente omitir el contexto y saltar a los argumentos.
Afortunadamente, se puede implementar fácilmente una función partial
para vincular solo argumentos.
Como esto:
función parcial(func, ...argsBound) { función de retorno (... argumentos) { // (*) return func.call(this, ...argsBound, ...args); } } // Uso: dejar usuario = { nombre: "Juan", decir(tiempo, frase) { alert(`[${time}] ${this.firstName}: ${phrase}!`); } }; // agrega un método parcial con tiempo fijo usuario.sayNow = parcial(usuario.say, nueva Fecha().getHours() + ':' + nueva Fecha().getMinutes()); usuario.sayNow("Hola"); // Algo como: // [10:00] Juan: ¡Hola!
El resultado de la llamada partial(func[, arg1, arg2...])
es un contenedor (*)
que llama func
con:
this
mismo que aparece (para user.sayNow
llame a su user
)
Luego le da ...argsBound
– argumentos de la llamada partial
( "10:00"
)
Luego le da ...args
– argumentos dados al contenedor ( "Hello"
)
Es muy fácil hacerlo con la sintaxis extendida, ¿verdad?
También hay una implementación _.partial lista de la biblioteca lodash.
El método func.bind(context, ...args)
devuelve una "variante vinculada" de la función func
que fija el contexto this
y los primeros argumentos, si se dan.
Por lo general, aplicamos bind
para solucionar this
en un método de objeto, de modo que podamos pasarlo a alguna parte. Por ejemplo, para setTimeout
.
Cuando arreglamos algunos argumentos de una función existente, la función resultante (menos universal) se llama parcialmente aplicada o parcial .
Los parciales son convenientes cuando no queremos repetir el mismo argumento una y otra vez. Por ejemplo, si tuviéramos una función send(from, to)
, y from
siempre debería ser la misma para nuestra tarea, podemos obtener un parcial y continuar con él.
importancia: 5
¿Cuál será el resultado?
función f() { alerta (esto); // ? } dejar usuario = { g: f.bind(nulo) }; usuario.g();
La respuesta: null
.
función f() { alerta (esto); // nulo } dejar usuario = { g: f.bind(nulo) }; usuario.g();
El contexto de una función vinculada está fijo. Simplemente no hay manera de cambiarlo más.
Entonces, incluso mientras ejecutamos user.g()
, la función original se llama con this=null
.
importancia: 5
¿Podemos cambiar this
mediante un enlace adicional?
¿Cuál será el resultado?
función f() { alerta(este.nombre); } f = f.bind( {nombre: "John"} ).bind( {nombre: "Ann" }); F();
La respuesta: Juan .
función f() { alerta(este.nombre); } f = f.bind( {nombre: "John"} ).bind( {nombre: "Pete"}); F(); // John
El objeto de función enlazada exótica devuelto por f.bind(...)
recuerda el contexto (y los argumentos, si se proporcionan) solo en el momento de la creación.
Una función no se puede volver a enlazar.
importancia: 5
Hay un valor en la propiedad de una función. ¿Cambiará después de bind
? ¿Por qué o por qué no?
función decir Hola() { alerta (este.nombre); } decir Hola.prueba = 5; dejar enlazado = decir Hola.bind ({ nombre: "Juan" }); alerta (encuadernado.prueba); // ¿cuál será el resultado? ¿por qué?
La respuesta: undefined
.
El resultado de bind
es otro objeto. No tiene la propiedad test
.
importancia: 5
La llamada a askPassword()
en el siguiente código debería verificar la contraseña y luego llamar a user.loginOk/loginFail
según la respuesta.
Pero esto lleva a un error. ¿Por qué?
Corrija la línea resaltada para que todo comience a funcionar correctamente (las demás líneas no se deben cambiar).
función preguntarContraseña(ok, falla) { let contraseña = solicitud("¿Contraseña?", ''); if (contraseña == "rockstar") ok(); de lo contrario falla(); } dejar usuario = { nombre: 'Juan', iniciar sesiónAceptar() { alert(`${this.name} inició sesión`); }, error de inicio de sesión () { alert(`${this.name} no pudo iniciar sesión`); }, }; preguntarContraseña(usuario.loginOk, usuario.loginFail);
El error se produce porque askPassword
obtiene las funciones loginOk/loginFail
sin el objeto.
Cuando los llama, naturalmente asumen this=undefined
.
bind
el contexto:
función preguntarContraseña(ok, falla) { let contraseña = solicitud("¿Contraseña?", ''); if (contraseña == "rockstar") ok(); de lo contrario falla(); } dejar usuario = { nombre: 'Juan', iniciar sesiónAceptar() { alert(`${this.name} inició sesión`); }, error de inicio de sesión () { alert(`${this.name} no pudo iniciar sesión`); }, }; preguntarContraseña(usuario.loginOk.bind(usuario), usuario.loginFail.bind(usuario));
Ahora funciona.
Una solución alternativa podría ser:
//... preguntarContraseña(() => usuario.loginOk(), () => usuario.loginFail());
Por lo general, eso también funciona y se ve bien.
Sin embargo, es un poco menos confiable en situaciones más complejas donde la variable user
puede cambiar después de llamar askPassword
, pero antes de que el visitante responda y llame () => user.loginOk()
.
importancia: 5
La tarea es una variante un poco más compleja de Reparar una función que pierde "esto".
El objeto user
fue modificado. Ahora, en lugar de dos funciones loginOk/loginFail
, tiene una única función user.login(true/false)
.
¿Qué deberíamos pasar askPassword
en el código siguiente, para que llame user.login(true)
como ok
y user.login(false)
como fail
?
función preguntarContraseña(ok, falla) { let contraseña = solicitud("¿Contraseña?", ''); if (contraseña == "rockstar") ok(); de lo contrario falla(); } dejar usuario = { nombre: 'Juan', iniciar sesión (resultado) { alerta (este.nombre + (resultado? 'iniciar sesión': 'no se pudo iniciar sesión')); } }; preguntarContraseña(?, ?); // ?
Sus cambios solo deberían modificar el fragmento resaltado.
Utilice una función contenedora, una flecha para ser conciso:
preguntarContraseña(() => usuario.iniciar sesión(verdadero), () => usuario.iniciar sesión(falso));
Ahora obtiene user
de las variables externas y lo ejecuta de forma normal.
O cree una función parcial desde user.login
que use user
como contexto y tenga el primer argumento correcto:
preguntarContraseña(usuario.login.bind(usuario, verdadero), usuario.login.bind(usuario, falso));