JS: Introducción a la POO

Teoría: Vinculación (bind)

JavaScript es un lenguaje de programación asíncrono. Debido a esto, las funciones a menudo se llaman como callbacks de otras funciones. Esto es especialmente común en el navegador, donde los callbacks se encadenan unos a otros. Hasta ahora, hemos estado trabajando con funciones simples, lo cual no ha presentado dificultades, pero todo cambia cuando se utilizan métodos.

En esta etapa, aún no hemos trabajado con asincronía, pero eso no debería dificultar la comprensión de la idea. En pocas palabras: la función setTimeout toma una función y un tiempo después del cual debe ser llamada. Cuando llega el momento, se ejecuta. Eso es todo.

Intenta ejecutar este código:

const printer = {
  name: 'Códica',
  print(greeting = 'hello') {
    console.log(`${greeting}, ${this.name}`);
  }
};

// Llamada directa
printer.print(); // => "hello, Códica"

Ahora queremos llamar al mismo código después de un segundo en lugar de inmediatamente. Para ello, utilizamos la función setTimeout(), que llama a la función pasada después de la cantidad de tiempo especificada.

// Queremos ejecutar el método print después de un segundo
// Asegúrate de ejecutar este código en tu computadora
// para experimentar cómo funciona setTimeout
// 1000 significa 1000 milisegundos o 1 segundo
// printer.print no es una llamada, sino una función pasada como argumento
setTimeout(printer.print, 1000);

// Después de un segundo
// => "hello, undefined"

Este código mostrará hello, undefined. ¿Por qué? Porque dentro de setTimeout() pasamos la función print() sin el objeto printer. Esto significa que la función pierde la conexión con el objeto y this ya no se refiere al objeto. Así es como se puede ilustrar lo que sucede:

const print = printer.print;
// En algún lugar dentro de setTimeout
print(); // => "hello, undefined"

Cuando no hay contexto, this se convierte en un objeto vacío si estamos hablando de funciones normales.

Este comportamiento a menudo no es deseado. Casi siempre, cuando se pasa un método, se supone que se llamará en el contexto del objeto al que pertenece. Hay varias formas de lograr este comportamiento. El más simple es envolver la función en otra función mientras la llamamos.

setTimeout(() => printer.print(), 1000);
// Después de un segundo
// => "hello, Códica"

// O sin setTimeout
const fn = () => printer.print();
// Todo funciona porque print() se llama desde printer
fn(); // => "hello, Códica"

Esta es una solución común que también ayuda a capturar variables externas cuando son necesarias para la llamada:

// Envolver en una función ayuda a pasar datos dentro
const value = 'hi';
setTimeout(() => printer.print(value), 1000);
// => "hi, Códica"

Bind

Otra forma es utilizar el método bind() (que significa "enlazar"). El método bind() está disponible en las funciones y su tarea es enlazar la función con algún contexto. El resultado de llamar a bind() será una nueva función que funciona como la función original, pero con el contexto vinculado a ella.

// El contexto es el mismo objeto printer en el que se define el método
// Esto parece bastante extraño, pero la vida es complicada
// bind se llama en la función y devuelve una función
const boundPrint = printer.print.bind(printer);

// Ahora podemos hacer esto
boundPrint(); // => "hello, Códica"
setTimeout(boundPrint, 1000);
// Después de un segundo
// => "hello, Códica"

// También se puede llamar a bind directamente en el lugar
// ya que devuelve una función
setTimeout(printer.print.bind(printer), 1000);
// hello, Códica

La función enlazada se fusiona con su contexto de forma permanente. this ya no cambiará.

Además del contexto, bind() acepta argumentos que la función necesita. No todos los argumentos de inmediato, sino cualquier parte de ellos. bind() los insertará en la nueva función (la que se devuelve del método bind()) "parcialmente". Esta técnica se llama "aplicación parcial de funciones". De esta manera, se pueden aplicar los argumentos necesarios de inmediato:

setTimeout(printer.print.bind(printer, 'hi'), 1000);
// Después de un segundo
// => "hi, Códica"

El enfoque con bind() solía ser popular antes de la aparición de las funciones de flecha, pero ahora se usa con poca frecuencia. Las funciones de flecha son más fáciles de entender y se utilizan ampliamente.

Apply & Call

bind() es útil cuando la vinculación del contexto y la llamada a la función ocurren en diferentes lugares y, por lo general, en momentos diferentes. Nos encontraremos con este tipo de código cuando lleguemos a la asincronía en JavaScript.

A veces, la llamada a funciones que utilizan this ocurre inmediatamente junto con la vinculación del contexto. Esto se puede hacer directamente llamando a la función devuelta por bind: ...bind(/* contexto */)():

const print = printer.print;
print.bind(printer)('hi'); // => "hi, Códica"

o se pueden utilizar las funciones especialmente creadas para esto, apply() y call():

// func.apply(thisArg, [ argsArray])
print.apply(printer, ['hi']); // hi, Códica

// func.call([thisArg[, arg1, arg2, ...argN]])
print.call(printer, 'hi'); // hi, Códica

Estas funciones hacen dos cosas: cambian el contexto y llaman a la función de inmediato. La diferencia radica en cómo manejan los argumentos de estas funciones: apply() toma los argumentos como una matriz como segundo parámetro, mientras que call() espera argumentos posicionales.

Estas funciones permiten hacer cosas bastante inusuales, como esto:

// Si no hay contexto, se pasa null
const numbers = [1, 10, 33, 9, 15];
const max = Math.max.apply(null, numbers); // 33

const numbers = [1, 10, 33, 9, 15];
const max = Math.max.call(null, ...numbers); // 33

La llamada anterior es solo una demostración y tiene poco uso práctico. El uso real de call() y apply() se muestra cuando se combina el contexto con funciones de prototipo. Hablaremos de esto en las próximas lecciones.