programación

Funciones Arrow

A continuación les comparto un abstracto de uno de los capítulos que estoy trabajando para un manual sobre JavaScript. Sugerencias y comentarios son bienvenidos.

Funciones Arrow

Anteriormente habíamos mencionado que a través de las funciones anónimas podiamos ahorrarnos el nombre de la función al momento de la declaración. ¿Qué tal si también pudiéramos ahorrarnos utilizar las palabras function y return? ¿Pudiéramos crear algo como esto?:

let myFunction = (){}

Bueno, dicha expresión produce el siguiente error: Uncaught SyntaxError: Unexpected token. El mensaje de error nos proveer una breve idea de lo que necesitamos para que podamos declarar la función. Dicho token o identificador es el famoso fat arrow o función flecha: =>. Para propósitos de claridad le llamaremos función arrow.

let myFunction = () => {}

Las ventajas primordiales de las funciones arrow son las siguientes:

  • Usan menos keywords. Ejemplo:
const myArray = [1, 2, 3];
const squares = myArray.map(x => x * x);
// No necesita utilizar el keyword function ni return

Veamos que significa todo esto a través de ejemplos. De todas esas variables especiales nos concentraremos en this.

Si fuéramos a utilizar todos los keyword que nos ahorramos con el uso de funciones arrow tendríamos algo así.

const myArray = [1, 2, 3];
const squares = myArray.map(function (x) {
  return x * x
});

Veamos como luce finalmente. Lo primero que vamos hacer es remover las palabras innecesarias, function y return.

const squares = myArray.map((x) => {x * x});

Hay ciertas reglas que podemos seguir a la hora de construir funciones arrow. Si solamente tenemos un parámetro, x en este caso, no necesitamos de paréntesis.

const squares = myArray.map(x => {x * x});

Además, en este ejemplo estamos devolviendo una expresión. Por lo tanto otra regla que podemos aplicar es, si la función devuelve solo una expresión, no necesitamos las llaves o corchetes.

const myArray = [1, 2, 3];
const squares = myArray.map(x => x * x);
// resultado de squares es [1, 4, 9]

Hermoso ¿no? Ahora veamos como se afecta el binding usando funciones arrow. Primero repasemos que eso del binding y su importancia.

¿Qué es this?

En JavaScript existe la palabra reservada this, que a su vez es un objeto, y está estrictamente relacionado al uso de funciones. Si deseamos saber que es this solo es necesario ejecutar un console.log(this) para explorar su contenido. El resultado de esto es el objecto window el cual representa el contenido del documento DOM y otras cosas más (funciones, métodos, etc), y solo está disponible en el navegador; es básicamente la raíz por donde se propagan todas las propiedades y objetos. Veamos un ejemplo.

Nota: En node.js no existe el objeto window.

console.log(this === window) // True

El ejemplo anterior claramente muestra que ambas cosas son lo mismo. Si creamos la siguiente variable:

var myName = 'Jaime'

Hacer un console.log de window.myName debe imprimir ‘Jaime’. Te recomiendo hagas el experimento.

Nota: No es un despiste que haya declarado la variable usando var en vez de let o const. El asunto aquí es que las variables declaradas var van directamente al objeto global window pero en cambio let y const están restringidas a un entorno declarativo que no puedes acceder a través del objeto window.

Sigamos con this y las funciones arrow para entender como se afectar el binding. Ya que el valor de this puede cambiar según una función es invocada, o sea, dependiendo del contexto en el cual se llama la función, es posible que confundamos el contenido del objeto. Cuando quizás deseabas acceder ciertas propiedades te das cuenta que estas obteniendo el resultado de otro objeto que muy probablemente no tiene lo que estas buscando o esperando. Recuerda, this es un objecto que va a depender de quien, donde y como se invoque la función. Y esto es importante tenerlo claro para entender como se comporta en las funciones arrow (y en generar cuando programas en JavaScript). Hagamos un ejemplo Franquestein usando la función arrow para calcular cuadrados pero dentro de un objeto (patrón muy común).

A continuación tenemos un objecto que contiene la funcionalidad de crear números al cuadrado. Digamos que se añade un nuevo requisito para sumar 1 a cada cuadrado.

const squares = {
  arr: [1, 2, 3],
  num: function () {
    return this.arr.map(function (x) {
      return (x * x) + this.add()
    })
  },
  add: function () {
    return 1
  }
}

console.log(squares.num())

El código anterior reportara un error indicando que this.add no es una función. Realmente no existe en el contexto de la función o funciones definidas en num, no hay un puente que me pueda llevar de num a add ya que this esta apuntando al objecto window y la función add existe solo en el contexto del objeto squares. Es importante entender esto. Para resolver el problema sin usar funciones arrow solo necesitamos traernos o redirigir this y pegarlo donde lo necesitemos. Eso lo hacemos a través del método bind.

const squares = {
  arr: [1, 2, 3],
  num: function () {
    return this.arr.map(function (x) {
      return (x * x) + this.add()
    }.bind(this))
  },
  add: function () {
    return 1
  }
}

Ahora efectivamente podemos usar la función add dentro de num. Hemos creado el puente utilizando bind. Con el uso de funciones arrows no tenemos que preocuparnos por este binding ya que al usar funciones arrow estas no manejan el binding como tradicionalmente se hace usando function. Como estamos tan entusiasmados con las funciones arrow, vamos a cambiar toda la función num a funciones arrow.

num: () => {
    return this.arr.map((x) => {
      return (x * x) + this.add()
    })
  }

¡HEY, STOP! No todo es color violeta. La documentación oficial indica que las funciones arrow son más adecuadas para funciones no relacionadas a métodos.

const squares = {
  arr: [1, 2, 3],
  num: () => {
    // Error, uso no aduacuado para funciones arrow
    return this.arr.map((x) => {
      return (x * x) + this.add()
    })
  },
  add: () => {
    return 1
  }
}

Finalmente nuestro ejemplo quedaría así:

const squares = {
  arr: [1, 2, 3],
  num: function () {
    return this.arr.map(x => (x * x) + this.add())
  },
  add: () => {
    return 1
  }
}

Si te revienta la cabeza, relax, toma tiempo digerir todo esto. Lo más importante es recordar que el objeto this dependerá de donde, quien y como invoca x o y función y que las funciones arrow utilizan el contexto de this que le ofrece el marco en donde son ejecutadas. Esto nos lleva al próximo tena que tiene que ver mucho con el lexical scoping en JavaScript.

¡Hasta la próxima!