Javascript #8 — Funciones II

Nauzet Hernández
8 min readNov 11, 2015

--

Como expliqué anteriormente, mediante funciones es posible lograr un flujo de datos que transforme un estado inicial y nos provea un resultado en un solo paso, bien usando la composición o la recursividad. Podemos definir y utilizar tantas como queramos, anidarlas, usarlas como argumentos a otras funciones, etc. Pero… ¿qué pasa cuando queremos usar una función que ya existe sin poder controlar su contexto?

Hay momentos en los que nos vendría muy bien usar cosas como map o reduce para otras cosas, usar los métodos de un objeto sobre otros objetos distintos, ó pasar parámetros por defecto a funciones que sabemos que se van a ejecutar en un contexto donde hay valores que no van a cambiar.

Es aquí donde entra a escena otra ventaja de que las funciones sean también objetos: sus propiedades. Cualquier objeto en Javascript puede ser modificado de forma dinámica, por tanto cualquier función es capaz de lo mismo. En este capítulo voy a plantear tres situaciones distintas y ver como podemos resolverlas modificando una función o su contexto.

Quiero que lo hagas por mí

A veces en Javascript nos encontramos con objetos que parecen una cosa pero realmente no lo son. Un ejemplo son los argumentos de una función o una query al DOM del navegador. Ambos objetos son listas de elementos, tienen un orden, podemos recorrerlos y se comportan como un array pero no podemos usar las funciones map o forEach por que no delegan en Array.prototype. Son lo que comúmente se llama array-like.

¿Cómo podríamos hacer, por ejemplo, para usar las funciones de Array.prototype sobre algo que no es un array? Con Function.call.

Todas las funciones delegan en el objeto Function, el cual tiene a su vez distintas funciones a las que podemos llamar. call nos permite ejecutar cualquier función usando el objeto que le pasemos como el this de la función. Pongamos un ejemplo.

En el contexto de una función siempre existe y se define el objeto arguments como una lista con los argumentos que hemos usado al llamarla. Podéis comprobarlo fácilemente haciendo lo siguiente.

function x(){ console.log(arguments) };
x(1, 2, 3, 4); // { '0': 1, '1': 2, '2': 3, '3': 4 }

Queremos por tanto usar esa lista para sumar todos los números que le pasemos, y queremos usar reduce para facilitar la tarea. Pero primero tenemos que generar algo que sea un array a partir de arguments, ya que como vemos arriba no es exactamente un array. Podemos hacerlo recurriendo a las funciones de Array.prototype, en concreto slice. Dicha función nos genera un array a partir de las posiciones que le digamos de otro, si no especificamos un rango lo devuelve entero.

function sum() {
return Array.prototype.slice.call(arguments)
.reduce((accumulator, num) => accumulator + num);
}

console.log(sum(1, 2, 3, 4, 5, 6)); // 21
console.log(sum(3, 4, 5, 6)); // 18

Básicamente le hemos dicho a slice que use arguments como this en su ejecución. call es algo similar al operador new que vimos en el herencia por prototipos, sólo que en lugar de crear un nuevo objeto usa el que le digamos.

slice es solo un ejemplo, podríamos usar cualquier función. call resulta útil cuando queremos generar un resultado usando una función que pertenece a otro objeto. Podemos además pasarle mas argumentos a call que se usarán como argumentos en la función original. Por ejemplo, si sólo queremos sumar los 3 primeros elementos, podemos decirle a slice las posiciones que tiene que copiar.

function sum() {
return Array.prototype.slice.call(arguments, 0, 3) // [0, 1, 2]
.reduce((accumulator, num) => accumulator + num);
}

console.log(sum(1, 2, 3, 4, 5, 6)); // '6'
console.log(sum(3, 4, 5, 6)); // '12'

Otra manera de conseguir lo mismo es usar Function.apply. Esta función hace lo mismo que call con la diferencia que acepta como segundo parámetro un array de elementos que usará como argumentos de forma individual en la función original. Dicho menos formal, convierte el array que le digamos en el arguments de la función.

Supongamos que queremos usar la función sum que definimos antes pasándole un array como argumento en lugar de hacerlo uno a uno separados por comas.

function sum() {
return Array.prototype.slice.call(arguments)
.reduce((accumulator, num) => accumulator + num);
}

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9];
console.log(sum(numbers)); // '[1, 2, 3, 4, 5, 6, 7, 8, 9]'

Como vemos no produce la suma. La función intenta sumar el array entero como un número y como no hace nada lo devuelve tal cual. Podemos arreglarla usando apply.

console.log(sum.apply(null, numbers)); // '45'

numbers se pasa como una lista de argumentos a sum y devuelve el resultado correcto. Hemos usado null como contexto porque en este caso no vamos a usar el this de la función ya que no es necesario. Pero no siempre es así.

Quiero hacerlo como tú

Podría darse el caso de que necesitemos hacer uso del contexto (el this en la función) porque queremos usar argumentos sino usar las propiedades que ya tiene el objeto. O por el contrario, en lugar de devolver algo queremos modificar sus propiedades. ¿Cómo hacemos en este caso? Cambiando el contexto.

Supongamos que tenemos dos objetos calculator que poseen números y realizan operaciones con los mismos. Sin embargo uno y otro hacen cálculos diferentes y almacenan el resultado en lugar de devolverlo.

const calc1 = { 
numbers: [7, 8, 9],
result: null,
factorial() {
this.result = this.numbers.reduce((a, b) => a * b, 1);
},

};

const calc2 = {
numbers: [3, 5, 6],
result: null,
sum() {
this.result = this.numbers.reduce((a, b) => a + b, 0);
}
};

calc1.factorial();
calc2.sum();
console.log(calc1.result); // '504'
console.log(calc2.result); // '14'

Vamos a usar call para que ambos objetos sean capaces de modificarse a si mismos y guardar un resultado usando las operaciones del otro.

calc1.factorial.call(calc2);
calc2.sum.call(calc1);
console.log(calc1.result); // '24'
console.log(calc2.result); // '90'

Le hemos dicho a calc1.factorial que se ejecute sobre calc2, y calc2.sum que lo haga sobre calc1. Como vemos, factorial y sum realizan un cálculo usando this.numbers y lo almacenan en this.result. Si cambiamos el this de las funciones hacemos que usen y modifiquen el objeto que le decimos. Cambiando el contexto conseguimos modificar un objeto usando una función que pertenece a otro objeto.

La composición nos permite copiar las propiedades de varios objetos para generar uno nuevo que las reúna todas. Pero no siempre necesitamos todo lo que implementa otro objeto para generar el nuestro. Podríamos tan solo copiar las propiedades que necesitamos, pero incluso puede no ser necesario porque solo vamos a usarlas para algo concreto una vez y luego no nos harán falta. apply y call nos ahorran la tarea.

Quiero que lo hagas como te digo

Hemos visto call y apply como formas de llamar una función. Pero... ¿que pasaría si en lugar de llamar una función necesitamos almacenarla? Entra en escena el tercer protagonista de hoy: Function.bind.

bind nos permite crear una nueva función cambiando su contexto y pudiendo asignarle los argumentos predefinidos que queramos. Conseguimos encapsular una función para poder usarla en otro sitio sin tener que conocer su contexto ni todos los argumentos que necesita. ¿Qué ventaja tiene ésto? ¿No puedo simplemente llamar a la función si la necesito? No siempre, a veces podemos necesitar usar esa función como parámetro de otra, y esa otra no tiene por qué saber que parámetros pasarle ni garantizar que el contexto sea el mismo. Es un caso muy común cuando tenemos eventos que necesitamos registrar, como el click de ratón, y hacer algo en base a dicha acción. Veamos un ejemplo.

Supongamos que tenemos una lista de ítems en un carrito de la compra de una web. Cada elemento tiene un ID único, una etiqueta descriptiva y un botón para eliminarlo del carrito. Necesitamos por tanto una función que se ejecute cada vez que eliminamos un elemento con dicho botón, y dicha función deberá además mostrar por consola que ítem se ha eliminado. Vamos a definirla.

let items = [
{ id: 1, name: 'Keyboard' },
{ id: 2, name: 'Mouse' },
{ id: 3, name: 'Monitor' }
];

function removeItem(id, name) {
items = items.filter(item => item.id !== id);
console.log(name, 'deleted');
}

removeItem(1, 'Keyboard'); // 'Keyboard deleted'

La función se ejecuta haciendo lo que esperamos porque le hemos dicho lo que debe hacer. ¿Que pasaría si la función la ejecuta alguien que no sabe que argumentos pasarle, como… un botón?

Nota: Aunque no hemos visto aún el uso de eventos y programación asíncrona, pondré un ejemplo sencillo para que se entienda por qué usar bind. El método addEventListener registra un evento sobre un elemento dentro del DOM y ejecuta la función que le pasamos como parámetro. Más adelante lo veremos en más detalle.

Tenemos por tanto tres ítems y necesitamos tres botones. A cada botón necesitamos decirle qué debe hacer, sin embargo no saben cual es el ID que tienen que usar para eliminar el ítem. Vamos por tanto a pasarles funciones que solo tengan que ejecutar sin preocuparse de qué parámetros pasarle.

button1.addEventListener('click', removeItem.bind(null, 1, 'Keyboard'));
button2.addEventListener('click', removeItem.bind(null, 2, 'Mouse'));
button3.addEventListener('click', removeItem.bind(null, 3, 'Monitor'));

Hemos creado 3 funciones distintas que no requieren usar parámetros ni conocer su contexto. Para verlo más claro lo pondré de otra forma.

const buttonAction1 = removeItem.bind(null, 1, 'Keyboard'); button1.addEventListener('click', buttonAction1);

Como vemos no estamos ejecutando ninguna función sino creando una nueva que le pasamos como parámetro. Para comprobar que funciona, podemos ver que pasaría creando una función con bind y ejecutándola.

const removeMouse = removeItem.bind(null, 2, 'Mouse'); removeMouse(); // 'Mouse deleted'

Pero en este ejemplo solo hemos asignado los parámetros y no hemos hecho uso del contexto. Pongamos un ejemplo dónde si sea necesario. Vamos a modificarlo para que los items estén dentro de un objeto y que éste se modifique con el método removeItem a través de this. Esta vez solo vamos a pasarle el ID y que sea el propio objeto quién busque su etiqueta y se encargue de todo.

const myCart = {
items: [
{ id: 1, name: 'Keyboard' },
{ id: 2, name: 'Mouse' },
{ id: 3, name: 'Monitor' }
],
removeItem(id) {
let item = this.items.filter(item => item.id === id)[0];
this.items = this.items.filter(item => item.id !== id);
console.log(item.name, 'deleted');
}
};

En este caso removeItem hace uso del this para modificarse así que usaremos la misma estrategia de antes. Pero hay un problema. Los eventos asignan a this el elemento que dispara la función (los botones) y no el objeto al que pertenence la función ( myCart en este caso). Si intentamos leer this.items al ejecutar el evento intentará buscar el objeto items en las propiedades del botón y no es lo que queremos.

Por tanto, en esta ocasion como primer parámetro tenemos que decirle que el contexto tiene que ser el objeto myCart.

button1.addEventListener('click', myCart.removeItem.bind(myCart, 1));
button2.addEventListener('click', myCart.removeItem.bind(myCart, 2));
button3.addEventListener('click', myCart.removeItem.bind(myCart, 3));

Con esto conseguimos que un objeto pueda ejecutar funciones de otro objeto sin que haya relación entre ellos ni sepa que parámetros necesita.

Conclusión

Tenemos por tanto funciones que no sólo hacen lo que queremos sino que pueden actuar de la forma que queremos sin que sepan como hacerlo. He tratado de explicar los conceptos de una forma resumida mediante los ejemplos más comunes para el uso de call, apply y bind; pero en adelante veremos nuevos casos en los que resultarán útiles estas prácticas y por qué hacen que el lenguaje sea tan rico y potente.

“Nuestro trabajo consiste en encontrar lo que ese tipo no sabe que necesita, pero necesita y luego asegurarnos de que sepa que lo necesita y que somos los únicos que podemos dárselo.” — Pirates of Silicon Valley (1999)

Originally published at https://nauzethdez.com on November 11, 2015.

--

--