Wextensible

Arreglando el problema con this con las funciones flecha

Figura
Figura. Una función flecha (Arrow Function) en ECMAScript ES6.

ECMAScript 6 (ES6) introduce las funciones flecha (arrow functions) que son una sintaxis para representar funciones anónimas fijando las referencias arguments, this, super y new.target de forma estática al alcance de la función. Para las funciones que ya conocemos estas referencias se resuelven de forma dinámica, con lo que su valor no existirá hasta que la función se ejecute. Esto es un problema en muchos casos, como cuando adjudicamos eventos a elementos HTML, puesto que en la función manejadora se resolverá el this al elemento sobre el que actúa el evento.

Aunque más abajo veremos los detalles de la sintaxis, por ahora dígamos que una función flecha es algo como (a,b,c) => { }, donde la palabra clave function se cambia por => y se anteceden los argumentos, siguiendo como siempre el cuerpo de código de la función.

En el fondo una función flecha es una función anónima con ciertas particularidades. Las dos funciones del siguiente código hacen exactamente lo mismo, no hay ninguna diferencia entre ellas dado que no usan las referencias arguments, this, super o new.target.

//Expresión de función anónima
let f = function(a) {
    return a;
};
console.log(typeof f); // "function"
console.log(f(1)); // 1
//Función flecha
let g = (a) => {
    return a;
};
console.log(typeof g); // "function"
console.log(g(1)); // 1
    
Con estos temas sobre JavaScript acompaño trozos de código mostrando resultados en la consola del navegador, tras probarlos en el actual Chrome 50 al redactar este tema.
Figura
Figura. Una función flecha es una expresión de función anónima.

En la Figura puede ver el estado de la función flecha g() del código anterior, función anónima que muestra las mismas propiedades que cualquier otra función, a excepción de prototype, puesto que las funciones flecha no pueden actuar como constructores.

El motivo para crear esta nueva sintaxis para una función anónima tiene que ver, como dijimos antes, en la forma en que las funciones tratan las referencias como this. Esta referencia es evaluada en el momento de ejecutar la función. Y por tanto es una referencia dinámica. Pero no siempre esto nos interesa y es por lo que se crean las funciones flechas para que esas referencias se evalúen de forma estática.

En el siguiente código tenemos una expresión de función fun() y una función flecha arw(). Estas funciones nos volcarán lo que hay en this y en arguments. También usamos arguments como un nombre de variable. Esto sólo lo podemos hacer si no estamos en modo estricto, como comentamos en el tema anterior sobre las restricciones al usar arguments. Disponemos también de un objeto literal y otro creado a partir de un constructor.

<script>
    //Una expresión de función expone this y arguments
    let fun = function(){
        console.log(this, arguments);
    };    
    //Una función flecha expone this y arguments
    let arw = ()=>{
        console.log(this, arguments);
    };
    //Usamos 'arguments' como un nombre de variable cualquiera
    let arguments = "abc";
    //Tenemos un objeto cualquiera
    let obj = {a: 1};
    //Un constructor con un par de métodos, uno es una 
    //expresión de función y otro es una función flecha
    function C(){
        this.b = 2;
        this.fun = function(){
            console.log(this, arguments);
        };
        this.arw = ()=>{
            console.log(this, arguments);
        };
    }
    //Instanciamos un objeto enviando un par de
    //argumentos al constructor
    let ob = new C(3, 4);

    //EJECUTAMOS AMBAS FUNCIONES
    //¿Qué obtenemos         En this          En arguments?
    //Para expresión función
    fun(1, 2);              // Window { .. }    [1, 2]
    fun.call(obj, 1, 2);    // Object {a: 1}    [1, 2]    
    ob.fun(1, 2);           // C {b: 2}         [1, 2]
    ob.fun.call(obj, 1, 2); // Object {a: 1}    [1, 2]
    //Para función flecha
    arw(1, 2);              // Window { .. }    "abc"
    arw.call(obj, 1, 2);    // Window { .. }    "abc"
    ob.arw(1, 2);           // C {b: 2}         [3, 4]
    ob.arw.call(obj, 1, 2); // C {b: 2}         [3, 4]
</script>
    

Veámos primero que sucede con la ejecución de la expresión de función. Haciendo fun(1, 2) vemos que this apunta a Window, como en la ejecución de cualquier función en el cuerpo principal de un script. Los argumentos pasados en la ejecución están ahora en arguments. Podemos cambiar al objeto {a: 1} con fun.call(obj, 1, 2) sin mayor problema. La ejecución del método ob.fun(1, 2) apuntará a su objeto {b: 2} creado a partir del constructor C(). Incluso con estos objetos podemos usar call() para cambiar de objeto. En todos los casos arguments traerá los argumentos de cada ejecución. Por lo tanto en cada ejecución se evalúan y actualizan this y arguments.

Y ahora veámos que hace la función flecha. Cuando declaramos arw() sucedía que en ese momento this apuntaba a Window. Y arguments era una variable con valor "abc". Eso quedo fijado en la función flecha para sus ejecuciones. Por eso arw(1, 2) y arw.call(obj, 1, 2) mantendrán ambas referencias en la ejecución. Veáse que ni siquiera call() puede cambiar el objeto destino de la ejecución en una función flecha. Por otro lado para los métodos del objeto de un constructor, cuando instanciamos ese objeto con new C(3, 4) el this se fija a ese objeto y arguments a los argumentos usados en el constructor. Por eso la ejecución de los métodos ob.arw(1, 2) y ob.arw.call(obj, 1, 2) mantendrán esas referencias en las ejecuciones.

Puede ver en ejecución el código anterior en el siguiente ejemplo:

Ejemplo: Diferencias entre expresiones de función y funciones flecha

EjecuciónEn thisEn arguments
Expresiones de función
fun(1, 2)
fun.call(obj, 1, 2)
Métodos con ob = new C(3, 4)
ob.fun(1, 2)
ob.fun.call(obj, 1, 2)
Funciones flechas
arw(1, 2)
arw.call(obj, 1, 2)
Métdos con ob = new C(3, 4)
ob.arw(1, 2)
ob.arw.call(obj, 1, 2)

Este ejemplo usa ES6 en modo normal, dado que hemos de redeclarar arguments como una variable. Puedes consultar el código JS original de este ejemplo.

Sintaxis de las funciones flecha en ES6

La sintaxis básica de una función flecha es un grupo de paréntesis con los parámetros, seguido de => y a continuación el clásico bloque de sentencias en el cuerpo de la función:

let arw = (par1, par2, ..., parN) => { 
    /* Cuerpo de la función */
};
    

Con las funciones flechas se agregan varias facilidades para simplificar el código. Si el cuerpo de la función sólo contiene una sentencia return que devuelve una expresión, podemos obviar el bloque y poner sólo la expresión:

let arwa = (a, b) => {return a+b;};
let arwb = (a, b) => a+b;
console.log(arwa(1, 2)); // 3
console.log(arwb(1, 2)); // 3
    

Si el cuerpo de una función sólo tiene una expresión que por si misma ejecuta lo que se espera de la función, podemos ponerla directamente. En este ejemplo console.log(a) es una expresión, pues las llamadas a funciones y métodos son expresiones. Por lo tanto podemos obviar el bloque de sentencias y poner sólo la expresión:

let arwa = (a) => {console.log(a);};
let arwb = (a) => console.log(a);
arwa(1); // 1
arwb(1); // 1
    

Podríamos usar un operador condicional o ternario como una expresión para disponer de algún control de flujo sobre la devolución:

let arw = (a, b) => (a>5) ? a+b : 0;
console.log(arw(3, 1)); // 0
console.log(arw(6, 1)); // 7
    

El identificador de función flecha => debe ir a continuación de los parámetros, sólo separado por espacios blancos que no sean saltos de línea. El cuerpo de la función o la expresión a devolver si puede ir en sucesivas líneas:

let arw = (a, b) => 
    (a>5) ? 
    a+b :
    0;
console.log(arw(6, 1)); // 7
    

Si ponemos un salto de línea entre los parámetros y la flecha => nos cursará un error de sintaxis:

let arw = (a, b)
    => a+b; // Unexpected token => 
    

Con un único parámetro podemos obviar los paréntesis:

let arwa = (a) => a+1;
let arwb = a => a+1;
console.log(arwa(3)); // 4
console.log(arwb(3)); // 4
    

Pero si no hay parámetros estos paréntesis son obligatorios:

let arw = () => [1, 2, 3];
console.log(arw()); // [1, 2, 3]
    

Si devolvemos un objeto hemos de encerrarlo entre paréntesis para diferenciar el bloque de sentencias del cuerpo de la función:

let arw = (x, y) => ({a: x, b: y});
console.log(arw(1, 2)); // Object {a: 1, b: 2}
    

Podemos seguir usando resto de parámetros, como para cualquier función:

let arw = (a, ...args) => a + args.join("");
console.log(arw(1, 2, 3, 4)); // "1234"
    

También podemos seguir usando parámetros por defecto, como para cualquier función:

let arw = (a=1, b=2) => a+b;
console.log(arw()); // 3
    

Podemos hacer una función flecha auto-ejecutable (IIFE) sin mayor problema.

((a, b) => 
    console.log(a+b) // 3
)(1, 2);
    

Es posible usar una función anónima como constructor de objetos:

let MiConstructor = function(x) {
    this.a = x;
};
let obj = new MiConstructor(1);
console.log(obj); // MiConstructor {a: 1}
console.log(obj.constructor); // function(x){this.a=x;}
    

Pero no podemos usar una función flecha como constructor. De hecho al inicio del tema comentamos que una función flecha dispone de todas las propiedades y métodos de cualquier función menos la propiedad prototype, puesto que es el objeto a partir del cual la función construirá nuevos objetos. Como las funciones flechas no construyen no disponen de esa propiedad.

let MiConstructor = (x) => {
    this.a = x;
};
//Error al instanciar un nuevo objeto. Nos dirá
//que 'MiConstructor is not a constructor''
let obj = new MiConstructor(1);
    

Como con cualquier función, podemos aplicar los métodos call(), apply() y bind() a una función flecha. Por supuesto, no podemos fijar el objeto destino tal como comentamos en el primer apartado, pues queda fijado el this de forma estática en la declaración de la función. Pero estos métodos nos permite manejar los argumentos con mayor facilidad para una función flecha. En el siguiente código medimos la distancia entre dos puntos (x0, y0) y (x1, y1). Luego aplicamos el método bind() para fijar los dos primeros argumentos y conseguir así una aplicación parcial de la función medir():

let medir = (x0, y0, x1, y1) => 
    Math.sqrt(Math.pow(x1-x0, 2) + Math.pow(y1-y0, 2));
console.log(medir(1, 1, 3, 3)); // 2.828427...
let medirOrigen = medir.bind(null, 0, 0);
console.log(medirOrigen(1, 1)); // 1.414213...
    

También es posible usar otra función flecha para fijar argumentos en lugar de usar bind(). Lo siguiente hace lo mismo que lo anterior con medirOrigen(x, y):

let medirOrigen = (x, y) => medir(0, 0, x, y);
console.log(medirOrigen(1, 1)); // 1.414213...
    

Podríamos haber hecho lo anterior con una expresión de función, pero con una función flecha resulta un código más simple e intuitivo. Con bind() sólo podemos fijar los argumentos de la izquierda, mientras que con una función podemos fijar cualquiera de los argumentos. En este caso fijamos los dos argumentos finales:

let medirFin = (x, y) => medir(x, y, 10, 10);
console.log(medirFin(1, 1)); // 12.727922...
    

Algunos usos de las funciones flecha en ES6

En el tema anterior comentábamos acerca del método bind() para fijar la referencia this en la declaración de un evento. En el apartado final de ese tema exponíamos un ejemplo con las posibilidades que teníamos para eso. Al final podíamos entender la ventaja de usar bind() para este cometido, pero que también era posible usar una función flecha. Y es que las funciones flechas están específicamente diseñadas para resolver problemas como el del this de los eventos. Ponemos el código de ese ejemplo otra vez:

    
<div id="conten"></div> 
<script>
function Constructor(prop){
    this.prop = prop;
    let div = document.createElement("div");
    div.textContent = this.prop;
    if (prop=="Falla") {
        div.addEventListener("click", this.metodo, false);  
    } else if (prop=="ThatThis"){
        let that = this;
        div.addEventListener("click", function(){
            that.metodo(2);
        }, false);
    } else if (prop=="Closure"){
        div.addEventListener("click", (function(that){
            return function(){
                that.metodo(3);
            };
        })(this), false);          
    } else if (prop=="Bind"){
        div.addEventListener("click", 
            this.metodo.bind(this, 4), 
            false);            
    } else if (prop=="Arrow") {
        div.addEventListener("click", 
            () => this.metodo(5), 
            false);
    }
    document.getElementById("conten").appendChild(div);
}
Constructor.prototype.metodo = function(num) {
    console.log(this, num);
};
//Creamos instancias del objeto
let elem1 = new Constructor("Falla");   // <div>Falla</div>      Event
let elem2 = new Constructor("ThatThis");// {prop: "ThatThis"}    2
let elem3 = new Constructor("Closure"); // {prop: "Closure"}     3
let elem4 = new Constructor("Bind");    // {prop: "Bind"}        4
let elem5 = new Constructor("Arrow");   // {prop: "Arrow"}       5
</script>
    

Las funciones flechas pueden hacer más claro el código, especialmente cuando hemos de poner una expresión de función como argumento de otra función. El método reduce() de un array toma sus elementos y les aplica una función (o callback) tomando parejas de elementos hasta reducir todo el array a un único valor. El ejemplo más simple es obtener la suma de los elementos de un array. Es evidente que una función flecha nos ofrece un código más claro y fácil de leer:

let valores = [1, 2, 3, 4, 5];
//Con expresiones de función
console.log(
    valores.reduce(function(a, b){return a+b;})
); // 120
//Con funciones flecha se hace más claro el código
console.log(
    valores.reduce((a, b) => a+b)
); // 120
    

En resumen, las funciones flecha son una nueva característica de ES6 que producen un código más legible y suscinto a la vez que resuelven el clásico problema con this.