JavaScript: Arrow functions

arrow-functions-1

Últimamente muchos desarrolladores de JavaScript hemos incorporado las arrow functions a nuestro código, por nuestra experiencia en otros lenguajes y/o por la facilidad para utilizarlas. Aquí explico lo que son, sus ventajas y cómo podemos utilizarlas.

Si has trabajado con lenguajes como C# o Java, las arrow functions (funciones flecha) no son otra cosa que el equivalente a una expresión lambda. Si te es suficiente con eso, gracias por la visita al blog. Si no, te explico:

¿Cómo funcionan las funciones?

Recordemos que en JavaScript, todo lo que se declara es un objeto. Variables de tipos nativos (String, Boolean, Number), arreglos, objetos, funciones… Sí, por si no sabías esto tampoco, las funciones en JavaScript también son objetos de tipo Function.

Es casi lo mismo escribir esto:

var testFunc = function (input) {
  return doSomething(input);
}

Que esto:

function testFunc(input) {
  return doSomething(input);
}

Digo casi lo mismo porque hay una leve diferencia: si lo declaramos como variable (la primera opción), debe ser inicializada antes de ser utilizada. Al igual que cualquier variable, tenemos que asignarle un valor (que en este caso es la definición de la función) antes de poder utilizarla. Si la declaramos como función (segunda opción), no tendremos este problema, pues las funciones son inicializadas antes de que el código se ejecute. Esto viene de una de las propiedades de JavaScript, llamada Hoisting.

El hecho de que una función sea una variable nos permite utilizarla en distintas formas. Por ejemplo, verificar si una función existe, o como parámetro dentro de otras funciones.

if (window.fetch !== undefined) {
  fetch('/api/getData')
    .then(function (result) {
      processResult(result);
    })
    .catch(handleError);
}

function handleError(error) {
  // Hacer algo con el error recibido.
}

En este ejemplo, estamos tratando de usar la función fetch para hacer una llamada a una URL y obtener algun resultado (lo que haríamos con XMLHttpRequest, pero en una sola línea). fetch no está soportado por todos los navegadores, y para no recurrir a browser sniffing, lo que hacemos primero es verificar si la función existe. Para esto la comparamos con undefined, sin poner los paréntesis, pues queremos usar su valor, no ejecutarla.

Después invocamos a la función, ahora sí con paréntesis y parámetro, y ésta nos devolverá un objeto Promise, el cual tiene dos funciones: then, que se ejecutará al obtenerse la respuesta de la solicitud, y catch, que se ejecutará si hay un error en la solicitud. Ambas reciben como parámetro una función.

En el caso de then, estamos definiendo la función en el mismo lugar donde se utilizará. Así como en fetch estamos poniendo un String directamente como parámetro, en then estamos poniendo una función. A esta práctica de definir una función dentro de otra se le llama closure.

Para catch, también estamos usando una función, la cual está definida por separado, y añadida como parámetro de catch a modo de variable, nuevamente, sin paréntesis.

Expresiones Lambda en otros lenguajes

Esa característica de JavaScript, de que las funciones sean variables, no se manejaba tal cual en otros lenguajes. En C#, por ejemplo, un método es una definición distinta a una variable. El método debe ser declarado en su espacio y utilizado solo mediante ejecuciones, no como variables, excepto en casos particulares, como la asignación de métodos como manejadores de eventos.

Más allá de eso, existía la necesidad de usar variables que representaran una función. Siguiendo con el ejemplo de C#, la introducción Linq requirió esa capacidad, lo que llevó a la incorporación de las llamadas expresiones lambda, que es básicamente eso: una manera de representar una función dentro de una variable, de una manera más simple que declarando un método como tal.

variable = (parámetros) => { cuerpo del método };

De este modo podemos definir un método dentro de otro y utilizarlo como variable o parámetro.

Arrow functions en JavaScript

arrow-functions-2

Ahora bien, las funciones flecha en JavaScript.

Como tal, las arrow functions no son necesarias, ya que la principal razón de existir en otros lenguajes ya estaba cubierta, e incluso con mayores capacidades que en los demás lenguajes.

Entonces, ¿para qué añadir funciones flecha en un lenguaje que no lo necesita? Principalmente, creo yo, por simplicidad. Las mismas capacidades que en otros lenguajes las tienen las arrow functions en JS:

var arrowFunc = (params) => { };
var returnArrowFunc = (params) => result;

En la primera función, se reciben uno o más parámetros y dentro de las llaves se define el cuerpo de la función.

El segundo caso es más sencillo. Si podemos reducir el cuerpo a una sola línea, podemos escribirlo sin llaves y sin la palabra return. El intérprete de JavaScript reconocerá esto como un return automático de la instrucción siguiente a la flecha, ya sea que ésta devuelve algo, o retornará undefined por default.

Si tenemos el siguiente código:

var array = getData(); // Método para obtener un arreglo de objetos.
var results = [];
for (var i = 0; i < array.length; i += 1) {
  var item = array[i];
  var itemResult = process(item);
  results.push(itemResult);
}

En lugar de crear el ciclo for y controlar manualmente el índice, podemos reemplazarlo con forEach, y en vez de usar un function normal, definirlo como función flecha:

var array = getData(); // Método para obtener un arreglo de objetos.
var results = [];
array.forEach(item => {
  var itemResult = process(item);
  results.push(itemResult);
});

No se ve mucha diferencia. Solo estamos ahorrándonos unos cuantos caracteres. Bueno, esa es prácticamente la única ventaja de las funciones flecha en JavaScript. Aunque bien, aquí todavía podemos ahorrarnos más caracteres.

Ya que el propósito del bloque for original es llenar un arreglo con los resultados de la función, podemos recurrir a otra función de Array, llamada map, que nos devuelve un subconjunto derivado de otro arreglo. Y ya que el contenido del bloque for únicamente llama a un método e inserta su resultado en el arreglo, podemos reducir la función flecha a una sola instrucción:

var array = getData(); // Método para obtener un arreglo de objetos.
var results = array.map(item => process(item));

Aquí sí se nota la ventaja de usar arrow functions, ya que estamos reduciendo considerablemente nuestro código.

El ejemplo que vimos al principio del artículo también puede ser convertido a función flecha.

fetch('/api/getData')
  .then(result => processResult(result))
  .catch(handleError);

Como dije, más allá de añadir ventajas a nuestro código, las arrow functions nos ayudan a simplificarlo, son fáciles de leer y nos ahorran un poco de código.

Otros ejemplos de arrow functions

Crear una promesa:

function doPromise() {
  return new Promise((resolve, reject) => {
    try {
      var result = doSomething();
      resolve(result);
    } catch (x) {
      reject(x);
    }
  });
}

doPromise().then(r => console.log(r)).catch(err => console.error(err));

Es importante que haya un solo elemento antes y después de la flecha. Si tenemos más de un parámetro, debemos encerrarlos en paréntesis. Pero si solo tenemos un parámetro, podemos omitirlos.

Usar fetch para obtener un resultado como HTML o como JSON:

function getHtml(url) {
  return fetch(url).then(result => result.text());
}
function getJson(url) {
  return fetch(url).then(result => result.json());
}

Las funciones text y json del objeto Response devuelven a su vez una promesa, la cual contiene el resultado como lo estamos esperando, sea texto o un objeto de JavaScript respectivamente.

Eliminar elementos repetidos de un arreglo:

Array.prototype.getUnique = function () {
  return this.filter((item, index, array) => array.indexOf(item) === index);
};

Algo similar al DISTINCT en SQL: la función filter, al igual que forEach, map, etc., puede recibir dos parámetros además del elemento de la iteración, que son la posición del elemento en el arreglo, y el arreglo de datos en sí. Ya que indexOf nos devuelve la primera posición de un valor dentro de un arreglo, ésta función nos devolverá solo aquellos cuyo índice es el primero de ese valor.

Cabe aclarar que esto solo funcionará para tipos primitivos. Ya que dos objetos complejos con las mismas propiedades y valores no son iguales para JavaScript.

Arrow function sin parámetros

setTimeout(() => {
  doSomething();
}, 1000);

Recordemos que no podemos poner la flecha sin un elemento predecesor. Por lo que, si necesitamos declarar una función flecha sin parámetros, lo que debemos hacer es poner los paréntesis vacíos.

Una de las características particulares de JavaScript es que los parámetros en las funciones son libres de uso. Una función puede tener definida cierta cantidad de parámetros que va a utilizar, y por otro lado, la instrucción que la ejecuta puede o no llevar esa cantidad. Al invocar una función podemos enviarle menos parámetros de los que la función tiene descritos, o incluso más (lo cual no tiene sentido en la mayoría de los casos, pero es posible).

Como ya vimos en las funciones para los arreglos, como findfilter o map, éstos reciben tres parámetros: el elemento de la iteración actual, el índice del mismo y el arreglo origen. Si no necesitamos alguno de los últimos parámetros, podemos omitirlos, mientras mantengamos el orden (es decir, si no usamos el segundo pero sí el tercero, no podemos omitir ninguno). Y de igual forma, si por alguna razón no necesitamos ninguno de los tres parámetros, podemos hacer lo mismo que en la de setTimeout: quitar todos los parámetros y dejar los paréntesis.

Convertir valores de un arreglo a numéricos:

var numbers = array.map(Number);
if (numbers.find((item, index) => isNaN(item)) {
  throw Error('Value in position ' + index + ' is not a number!');
}

Éste es uno de mis ejemplos favoritos, pues muestra una de las diferencias de JavaScript con otros lenguajes. Suponiendo que recibimos un arreglo de datos, por ejemplo, desde un textarea, separados por coma o salto de línea, y esos datos deben ser numéricos, podemos simplemente mapearlos a un nuevo arreglo. Y no tenemos nuestra función flecha, pues ya existe la función Number en JavaScript. Esta función recibe un valor y devuelve su resultado como numérico. Usando esta función ya existente, solo la ponemos como parámetro de map.

Lo siguiente es verificar si hay algun elemento inválido. La función find nos devuelve el primer elemento de una lista que cumpla la función del parámetro, por lo que la usamos para checar si algun valor no es numérico (Number nos devolvería un NaN como resultado para ese valor), y de ser así, arrojamos el error.

Devolviendo objetos complejos desde arrow functions

var arr = [1, 2, 3, 4];
var result = arr.map(id => ({
  number: id,
  item: getItem(id)
}));

Aquí estamos definiendo una arrow function que devolverá un objeto complejo. Tenemos la posibilidad de reducir el cuerpo de la función a una sola instrucción, que es la definición del objeto resultante, lo cual nos permite eliminar las llaves que encierran a la función. Pero por otro lado, un objeto complejo se define en notación JSON envuelto en llaves. Es decir, queremos omitir las llaves que envuelven a la función, pero necesitamos las que envuelven al objeto.

Si lo que sigue a la flecha es una llave, el motor de JavaScript lo interpretará como el inicio del cuerpo de la función, no como una definición de objeto. Por esto es necesario envolver el objeto entre paréntesis, para que sea considerado como tal.

Compatibilidad en navegadores

Las arrow functions están soportadas en todos los navegadores modernos en sus versiones más o menos recientes. La última versión de Internet Explorer no lo soporta, pero la primera de Edge sí.

A diferencia de otros elementos modernos en JavaScript, como son las funciones de Array que usamos en los ejemplos, la función fetch u otras características nuevas del lenguaje, las funciones flecha no pueden ser añadidas a versiones de JS que no las soportan, ya que no son clases o funciones, sino una estructura nueva de sintaxis. Cualquier navegador obsoleto que lea los símbolos => juntos en el código lo interpretará como un error de sintaxis. Si necesitamos soportar navegadores como estos tendríamos que usar otras herramientas, como Babel, que pueden transformar el código que nosotros escribimos en una versión nueva de JavaScript, a una que sea soportada por navegadores más viejos.

NodeJS, que está basado en versiones modernas de Webkit, también lo soporta. De igual forma se pueden usar en TypeScript, incluso permitiéndonos definir el tipo de cada parámetro, y siendo que el código en este lenguaje es “compilado” a JavaScript, serán interpretadas acorde a la configuración de versión de EcmaScript en nuestro proyecto, como funciones flecha o como funciones regulares.

Conclusión

Las arrow functions tal vez no tengan alguna ventaja técnica para nuestro código. Sin embargo, son fáciles de entender, ayudan a tener un código más limpio, y nos ahorran unos cuantos caracteres, lo cual se agradece cuanto más crece nuestra aplicación.

Y en el peor de los casos, siempre pueden ser fácilmente convertidas a funciones regulares.

Anuncios

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión /  Cambiar )

Google photo

Estás comentando usando tu cuenta de Google. Cerrar sesión /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión /  Cambiar )

Conectando a %s