Entendiendo Bind, Apply y Call en Javascript

Una de las cosas más complicadas de Javascript, sobretodo para los usuarios que acaban de llegar, es el uso de this y, por ende, de las funciones bind, apply y call. Te intento explicar qué hacen estas funciones y cómo usarlas correctamente con ejemplos.
Entendiendo Bind, Apply y Call en Javascript

This en Javascript

Antes de ver qué hacen estas funciones es necesario repasar un poco de teoría. Es raro que Javascript sea el primer lenguaje de programación que aprendes; generalmente habrás aprendido Java, Python, PHP o C#. En la mayoría de ellos hay una regla muy básica que habrás aprendido en Programación Orientada a Objetos: this hace referencia a la instancia de la clase que estás codificando.

public void setNombre(String x) {
    this.nombre = x;
    this.myFunc();
}

El código anterior es claro: dentro de esta instancia de clase, establece la variable nombre a lo que valga el argumento x y después llama al método (función dentro de la clase) llamado myFunc. ¡ Genial ! Lo tenemos.

Pero, un segundo. ¿Qué hace this en un lenguaje que no es orientado a objetos 🤔? La respuesta es simple pero un poco compleja: this en Javascript hace referencia al contexto. La misma respuesta pero un poco más refinada es que this depende de cómo se llame a dicha función; o dicho de otra forma, depende del contexto.

El contexto en Javascript

Una forma más fácil de entender el contexto en Javascript es usando la siguiente frase: "this es el objeto propietario de la función (el cual la está llamando)". Puede que a priori no tenga mucho sentido, pero vamos a ver varios ejemplos para entender mejor qué es eso del contexto en Javascript y para entender un poco después, cómo funciona bind y sus hermanas apply y call.

Vamos a ver el ejemplo más simple: cuando el contexto es el objeto global (el objeto global será window en el navegador, o global en Node.js). El thisde la función myFunction representa al objeto global porque ha sido el objeto global quién ha llamado a dicha función (no se ha llamado desde una estructura).

function myFunction() {
  this.a = 2;
}

myFunction();
console.log(a); // Imprime 2    

Nota: Este ejemplo no funcionaría si usamos el modo estricto. Pero tómatelo como un ejemplo de aprendizaje

Hasta aquí bien ¿verdad? Pero vamos a complicarlo un poco más. ¿Qué ocurre si definimos la función dentro de un objeto ? Imagina (o más bien prueba) el siguiente código.

const persona = {
    name: 'Pepín',
    surname: 'Fernandez',
    fullname: function() {
        console.log(this) // OJO !!
        return this.name + ' ' + this.surname
    }
}


persona.fullname()

// El console.log imprimirá lo siguiente:
// { name: 'Pepín', surname: 'Fernandez', fullname: [Function: fullname] }

Fíjate en que el console.log está imprimiendo el this y ese this vale lo mismo que el objeto persona. ¿Por qué? Porque el propietario de la función fullname es el objeto persona.

Este ejemplo es un poco más retorcido, pero para que te des cuenta de una cosa que suele pasar (y en algún momento te pasará según vayas trabajando en Javascript). Está literalmente sacado de MDN. Si te fijas hay dos llamadas a la función getX y, realmente, las dos llamadas son a la misma función: lo único que cambia es el contexto. En la primera llamada (la identificada como A), la función devuelve 81 porque estás usando el contexto donde this.x vale 81 (el this en getX es el propio objeto). Sin embargo, cuando copias la función en la llamada B cambia el contexto (la función sigue funcionando exactamente igual, pero cambias el contexto, pues this.x = 9 en el contexto global).

this.x = 9;
var module = {
  x: 81,
  getX: function() { return this.x; }
};

module.getX(); // Llamada A: devuelve 81

var getX = module.getX;
getX(); // Llamaba B: devuelve 9, 
        //porque en este caso, "this" apunta al objeto global

Las funciones se pueden copiar sin problemas en Javascript porque es un tipo de variable: function. El problema viene cuando copias una función pero cambia el contexto.

Ten en cuenta que las funciones de flecha gorda ( => ) no crean un nuevo contexto.

Más o menos vamos entendiendo el contexto en Javascript: el objeto propietario de la función a la que se ha llamado (y si no se especifica, el objeto global). Pero, ¿qué ocurre si quiero cambiar el contexto de una función? ¿Hay algúna forma de hacer eso? Exacto, con las funciones bind, call y apply.

La función Bind en Javascript

fun.bind(thisArg[, arg1[, arg2[, ...]]])

La función bind() de Javascript básicamente hace una copia de la función original y la deja igual salvo porque cambia el contexto al que apunta this. Está disponible en todas las variables que representan funciones.

Rescatemos el anterior ejemplo. Declaramos el objeto que contiene a la función y posteriormente usamos bind() sobre el método getX del objeto module. Ese bind le está diciendo a Javascript: "Oye! Copia la función pero this va a valer {x: 5}".

var module = {
  x: 81,
  getX: function() { return this.x; }
};


const getX = module.getX.bind({x: 5});
getX(); //Devuelve 5

Fíjate en que no se usa getX().bind(). Eso significaría que ejecutase getX y sobre el resultado que devuelva hacemos el bind.

Adicionalmente, es posible pasarle argumentos a una función clonada con bind. En el siguiente código vemos como saludo es una copia de obj.saludaConEdad pero cambiando el contexto para que el nombre sea el de fulanito. Sin embargo, saludo2 es una copia de obj.saludaConEdad donde también se establece que como primer argumento (edad) siempre se llamará con 10. Esto no es tan útil en la función bind como en las demás, pero está disponible para que la copia incluya ya el argumento o argumentos. Como la función bind copia la función, generalmente solo se le establece el primer parámetro (el que define el this). El resto de parámetros se establecen cuando se llama a la copia.

var obj = {
    nombre: 'Pepín',
    saludaConEdad: function(edad) {
      return 'Soy ' + this.nombre + ' y tengo ' + edad + ' años';
    }
}


obj.saludaConEdad(6); //'Soy Pepín y tengo 6 años'
const saludo = obj.saludaConEdad.bind({nombre: 'fulanito'})
saludo(7); //'Soy fulanito y tengo 7 años'
const saludo2 = obj.saludaConEdad.bind({nombre: 'fulanito'}, 10);
saludo2(); //'Soy fulanito y tengo 10 años'

La función Call en Javascript

function.call(thisArg[, arg1[, arg2[, ...]]])

La función call es parecida a bind salvo por una cosa: call no hace una copia de la función. Es decir, mientras que la función bind hace una copia de la función que vamos a copiar (que se puede guardar en una variable para usarla posteriormente), call ejecuta directamente la función (pero antes establece el this personalizado). Cuando se usa call(), sí se suelen definir el segundo argumento y sucesivos, porque no hay otra forma de indicarlos a la hora de llamar a la función que estamos clonando.

obj.saludaConEdad(6); //'Soy Pepín y tengo 6 años'

// Soy fulanito y tengo undefined años.
// Como no se ha pasado primer argumento, la función no lo puede leer
obj.saludaConEdad.call({nombre: 'fulanito'})

// Soy fulanito y tengo 66 años
obj.saludaConEdad.bind({nombre: 'fulanito'}, 66);

La función Apply en Javascript

fun.apply(thisArg[, argsArray])

La función Apply es exactamente igual que Call salvo por un pequeño detalle: los argumentos de la función original se pasan por array (no como una lista de argumentos). Esto es útil cuando tenemos los argumentos guardados en un arreglo y no tenemos que preocuparnos por ellos. Quitando esa diferencia, técnicamente es igual que call.

obj.saludaConEdad(6); //'Soy Pepín y tengo 6 años'

// Soy fulanito y tengo undefined años.
// Como no se ha pasado array, la función no lo puede leer
obj.saludaConEdad.apply({nombre: 'fulanito'})

// Soy fulanito y tengo 66 años
obj.saludaConEdad.apply({nombre: 'fulanito'}, [66]);

Conclusión

Las funciones bind(), call() y apply() establecen un contexto, reemplazando el valor de this dentro de la función que están llamando (o copiando). Pero hay algunas sutiles diferencias:

  • Bind copia la función
  • Call ejecuta directamente la función sin copiarla
  • Apply es como Call, pero entrega a la función original los parámetros en forma de array.

Sin duda, el contexto y el uso de la palabra reservada this es una de las cosas más complicadas de aprender de Javascript. Sin embargo, espero que después de estos apuntes te haya quedado un poco más claro cómo usarlo correctamente y evites pasarte horas y horas depurando funciones que "no encuentran las variables". Claro que no las encuentran: no estás usando el contexto adecuado.

Que tengas un feliz coding 😀