Vinculación de Funciones con bind
Al pasar métodos de un objeto como callbacks, por ejemplo a setTimeout
, surge un problema común: la «pérdida de this
«.
En este capítulo, exploraremos las formas de resolverlo.
Pérdida de this
Hemos visto ejemplos de pérdida de this
. Cuando un método se separa de su objeto, this
se pierde.
Así es como puede suceder con setTimeout
:
let user = {
firstName: "John",
sayHi() {
alert(`Hola, ${this.firstName}!`);
}
};
setTimeout(user.sayHi, 1000); // Hola, ¡undefined!
Como vemos, el resultado no muestra “John” como this.firstName
, ¡sino undefined!
Esto ocurre porque setTimeout
recibe la función user.sayHi
, separándola del objeto. La última línea podría reescribirse como:
let f = user.sayHi;
setTimeout(f, 1000); // user pierde el contexto
El método setTimeout
en el navegador es especial: establece this = window
para la llamada a la función (en Node.js, this
se convierte en el objeto timer, pero no importa aquí). Entonces, this.firstName
intenta obtener window.firstName
, que no existe. En otros casos similares, this
se vuelve undefined
.
El desafío es típico: queremos pasar un método de un objeto a otro lugar (aquí, al temporizador) donde se llamará. ¿Cómo asegurarnos de que se llamará en el contexto correcto?
Solución 1: un Contenedor
La solución más simple es usar una función contenedora:
let user = {
firstName: "John",
sayHi() {
alert(`Hola, ${this.firstName}!`);
}
};
setTimeout(function() {
user.sayHi(); // Hola, John!
}, 1000);
Ahora funciona, porque toma user
del entorno léxico externo y luego llama al método normalmente.
Podemos hacerlo de otra manera también:
setTimeout(() => user.sayHi(), 1000); // Hola, John!
Esto funciona, pero tiene una pequeña vulnerabilidad en la estructura de nuestro código.
¿Qué sucede si antes de que setTimeout
se dispare (hay un segundo de retraso) user
cambia su valor? Entonces, de repente, ¡llamará al objeto incorrecto!
let user = {
firstName: "John",
sayHi() {
alert(`Hola, ${this.firstName}!`);
}
};
setTimeout(() => user.sayHi(), 1000);
// ...el valor de user cambia en 1 segundo
user = {
sayHi() { alert("¡Otro user en setTimeout!"); }
};
// ¡Otro user en setTimeout!
La siguiente solución garantiza que eso no sucederá.
Solución 2: bind
Las funciones tienen un método integrado bind
que permite fijar this
.
La sintaxis básica es:
let boundFunc = func.bind(context);
El resultado de func.bind(context)
es un “objeto exótico”, una función similar a una función regular que se puede llamar como función; esta pasa la llamada de forma transparente a func
estableciendo this = context
.
En otras palabras, llamar a boundFunc
es como llamar a func
pero con un this
fijo.
Por ejemplo, aquí funcUser
pasa una llamada a func
con this = user
:
let user = {
firstName: "John"
};
function func() {
alert(this.firstName);
}
let funcUser = func.bind(user);
funcUser(); // John
Aquí, func.bind(user)
es como una “variante vinculada” de func
, con this = user
fijo en ella.
Todos los argumentos se pasan a la función original “tal cual”, por ejemplo:
let user = {
firstName: "John"
};
function func(phrase) {
alert(phrase + ', ' + this.firstName);
}
// vincula this a user
let funcUser = func.bind(user);
funcUser("Hola"); // Hola, John (se pasa el argumento "Hola", y this=user)
Ahora intentemos con un método de objeto:
let user = {
firstName: "John",
sayHi() {
alert(`Hola, ${this.firstName}!`);
}
};
let sayHi = user.sayHi.bind(user); // (*)
// puede ejecutarlo sin un objeto
sayHi(); // Hola, John!
setTimeout(sayHi, 1000); // Hola, John!
// incluso si el valor de user cambia en 1 segundo
// sayHi usa el valor pre-enlazado
user = {
sayHi() { alert("¡Otro user en setTimeout!"); }
};
En la línea (*) tomamos el método user.sayHi
y lo vinculamos a user
. sayHi
es una función “vinculada”. No importa si se llama sola o se pasa a setTimeout
, el contexto será el correcto.
Aquí podemos ver que los argumentos se pasan “tal cual”, solo que this
se fija mediante bind
:
let user = {
firstName: "John",
say(phrase) {
alert(`${phrase}, ${this.firstName}!`);
}
};
let say = user.say.bind(user);
say("Hola"); // Hola, John! ("Hola" se pasa a say)
say("Adiós"); // Adiós, John! ("Adiós" se pasa a say)
Método de Conveniencia: bindAll
Si un objeto tiene muchos métodos y planeamos pasarlo activamente, podríamos vincularlos a todos en un bucle:
for (let key in user) {
if (typeof user[key] == 'function') {
user[key] = user[key].bind(user);
}
}
Las bibliotecas de JavaScript también proporcionan funciones para un enlace masivo, ej. _.bindAll(object, methodNames)
en lodash.
Funciones Parciales
Hasta ahora solo hemos hablado de vincular this
. Vamos un paso más allá.
Podemos vincular no solo this
, sino también argumentos. Es algo que no suele hacerse, pero a veces puede ser útil.
Sintaxis completa de bind
:
let bound = func.bind(context, [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)
:
function mul(a, b) {
return a * b;
}
Usemos bind
para crear, en su base, una función double
para duplicar:
function mul(a, b) {
return a * b;
}
let double = mul.bind(null, 2);
alert( double(3) ); // = mul(2, 3) = 6
alert( double(4) ); // = mul(2, 4) = 8
alert( double(5) ); // = mul(2, 5) = 10
La llamada a mul.bind(null, 2)
crea una nueva función double
que pasa las llamadas a mul
, fijando null
como contexto y 2 como primer argumento. Los demás argumentos se pasan “tal cual”.
Esto se llama aplicación parcial: creamos una nueva función fijando algunos parámetros a la existente.
Note que aquí no usamos realmente this
. Pero bind
lo requiere, por lo que debemos poner algo como null
.
La función triple
en el siguiente código triplica el valor:
function mul(a, b) {
return a * b;
}
let triple = mul.bind(null, 3);
alert( triple(3) ); // = mul(3, 3) = 9
alert( triple(4) ); // = mul(3, 4) = 12
alert( 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 usarla y evitamos proporcionar el primer argumento cada vez, ya que se fija 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 para mayor comodidad.
Por ejemplo, tenemos una función send(from, to, text)
. Luego, dentro de un objeto user
podemos querer usar una variante parcial del mismo: sendTo(to, text)
que envía desde el usuario actual.
Parcial sin Contexto
¿Qué pasa si queremos fijar algunos argumentos, pero no el contexto this
? Por ejemplo, para un método de objeto.
El método bind
nativo no permite eso. No podemos simplemente omitir el contexto y saltar a los argumentos.
Afortunadamente, se puede implementar fácilmente una función parcial para vincular solo argumentos.
Como esto:
function partial(func, ...argsBound) {
return function(...args) {
return func.call(this, ...argsBound, ...args);
}
}
// Uso:
let user = {
firstName: "John",
say(time, phrase) {
alert(`[${time}] ${this.firstName}: ${phrase}!`);
}
};
// agregar un método parcial con tiempo fijo
user.sayNow = partial(user.say, new Date().getHours() + ':' + new Date().getMinutes());
user.sayNow("Hola");
// Algo como:
// [10:00] John: Hola!
El resultado de la llamada partial(func [, arg1, arg2 ...])
es un contenedor que llama a func
con:
- El mismo
this
(para la llamada auser.sayNow
esuser
) - Luego le da
...argsBound
: argumentos desde la llamada apartial
(«10:00») - Luego le da
...args
: argumentos dados desde la envoltura («Hola»)
Muy fácil de hacer con la sintaxis de propagación, ¿verdad?
También hay una implementación preparada _.partial
desde la librería lodash.
Resumen
El método func.bind(context, ...args)
devuelve una “variante vinculada” de la función func
, fijando el contexto this
y, si se proporcionan, también los primeros argumentos.
Por lo general, aplicamos bind
para fijar this
a un método de objeto, de modo que podamos pasarlo a otro lugar. Por ejemplo, en setTimeout
.
Cuando fijamos algunos argumentos de una función existente, la función resultante (menos universal) se llama aplicación parcial o parcial.
Los parciales son convenientes cuando no queremos repetir el mismo argumento una y otra vez. Por ejemplo, si tenemos una función send(from, to)
, y from
siempre debe ser igual para nuestra tarea, entonces, podemos obtener un parcial y continuar la tarea con él.