Introducción
En esta ocasión vamos a estudiar y analizar una de esas curiosidades del lenguaje Javascript poco explotadas como es el operador coma. Se trata de una construcción sintáctica algo confusa cuyo uso solemos evitar para no añadir más complejidad a la lectura de un código. No obstante, conocerla implica adelantarnos un paso a las técnicas de minificado y compresión habitualmente utilizadas por la mayoría de herramientas al uso.
Por ser una parte más del lenguaje Javascript, no está de más echarle un vistazo a fondo para luego decidir si se acomoda o no a nuestro estilo.
Vamos a ello!
Descripción
El operador coma en Javascript tiene como objetivo separar múltiples expresiones para evaluarlas una a una, de izquierda a derecha, devolviendo el valor del último operando. Su sintaxis, tal y como podemos ver en la especificación ECMAScript 2015, es la siguiente:
AssigExpression[?In, ?Yield] Expression[?In, ?Yield] , AssigExpression[?In, ?Yield] |
O expresado de forma más clara:
expr1, expr2, expr3... |
Esto quiere decir que el intérprete recibe varias expresiones encadenadas (de asignación y/o valor) para pasar a evaluar/resolver/computar cada una de las mismas en estricto orden para, finalmente, retornar el último valor obtenido.
NOTA: En Javascript tenemos dos tipos de expresiones: aquellas que asignan un valor a una variable y aquellas que simplemente computan/operan con un valor dado o generado.
// Expresiones de asignación (signo =) var foo, bar; foo = 'Hello', bar = 'World'; // "World" // Expresiones de valor (no hay asignación) 2 * 1, 2 * 2, 2 * 3, 2 * 4; // 8 // Expresiones combinadas var foo, bar; foo = 'Hello', 4 * 2, bar = 'World'; // "World" |
NOTA: Un detalle importante es distinguir entre el operador coma que estamos tratando en esta entrada y la coma que utilizamos para separar variables con ‘var’: ¡no son lo mismo!. Si quieres saber más sobre esto, al final de este artículo tenemos una breve aclaración al respecto.
Importante: prioridad
Es interesante recalcar que no se pueden modificar la prioridad de las expresiones usando por ejemplo paréntesis: el orden siempre es estrictamente lineal.
var foo, bar; foo = 'Hello', 4 * 2 , ( bar = 'World' ); // "World" |
Los paréntesis, en este escenario, solo sirven para agrupar expresiones de forma lógica:
var r1, r2, r3; var foo, bar, foobar; r1 = ( foo = ( 1, 2 ) ); r2 = ( bar = ( 1 ), 2 ); r3 = ( foobar = ( 1 , 2 ) ); console.info( foo ); // 2 console.info( bar ); // 1 console.info( foobar ); // 2 console.info( r1 ); // 2 console.info( r2 ); // 2 console.info( r3 ); // 2 |
Utilizando funciones como expresiones
Dada la flexibilidad de Javascript, nada nos impide utilizar funciones como operandos cuando utilizamos el operador coma. La cadena continúa devolviendo de este modo el último valor obtenido:
var double = function ( x ) { return x * 2; }; var triple = function ( x ) { return x * 3; } double( 2 ), triple( 2 ); // 6 |
Mismo ejemplo pero con otros valores y sintaxis moderna de Las funciones flecha (hasta que nos acostumbremos!!):
var double = ( x ) => x * 2, triple = ( x ) => x * 3; double( 4 ), triple( 4 ); // 12 |
Si alguna función de la cadena lanza una excepción, la ejecución se detiene inmediatamente al alcanzarla sin continuar evaluando otros operandos:
var foo = () => { console.info( 'step 1' ); }, bar = () => { throw 'step 2'; }, foobar = () => { console.info( 'step 3' ); }; foo(), bar(), foobar(); // step 1 // Error: step 2 |
Como cabría esperar, y ya hemos visto más arriba, podemos almacenar el resultado de una cadena de expresiones para reutilizarlo más adelante:
var double = ( x ) => x * 2, triple = ( x ) => x * 3; var result = ( double( 4 ), triple( 4 ) ); console.info( result ) // 12 |
NOTA: Obsérvese como es necesario encerrar las expresiones entre paréntesis para poder recoger su valor de retorno, en este caso el de la última operación computada. Si no usamos estos paréntesis, obtendríamos un error de sintaxis.
Bucles exóticos
El uso del operador coma está muy ligado al trabajo con bucles y resulta relativamente frecuente verlo dentro de los parámetros de una instrucción ‘for‘. De este modo, jugando con las evaluaciones y los valores de retorno, podemos obtener comportamientos muy interesantes:
Llamar a una función n + 1 veces
Si nuestro bucle está diseñado para iterar n veces, podemos ejecutar una función n + 1 veces usando el operador coma junto a la condición:
var log = function () { console.warn( 'x value: ', x ); }; for ( var x = 0; log(), x < 5; x++ ) { console.info( x ); } /* x value: 0 0 x value: 1 1 x value: 2 2 x value: 3 3 x value: 4 4 x value: 5 */ |
El código anterior, aunque prevee solo 5 iteraciones, llamará a la función log 6 veces. Esto es así porque el intérprete invoca dicha función antes de comprobar la condición de salida.
Ejecutar una misma función tras cada iteración
Si agregamos una función con el operador coma junto a la expresión final (tercer parámetro/argumento del for), ésta se ejecutará tras cada iteración:
var log = () => { console.warn( 'x value: ', x ); }; for ( var x = 0; x < 5; log(), x++ ) { console.info( x ); } /* 0 x value: 0 1 x value: 1 2 x value: 2 3 x value: 3 4 x value: 4 */ |
Esto funciona porque al superarse la condición para entrar en una nueva iteración (x < 5), ésta se evalúa para luego ejecutar la expresión final (log(), x++).
En este punto, podemos volvernos locos y manipular el valor de la condición del bucle (x) desde nuestra función log… En un escenario donde se tiene que repetir una operación en caso de error (peticiones asíncronas, escritura en BD, error de latencia, …), podría resultar útil. Personalmente, no lo usaría nunca en Producción, pero como ejercicio teórico nos vale 😉
Un ejemplo de lo anterior, casi en pseudo código, sería el siguiente:
var log = function () { // If something went wrong var fakeCondition = false; if ( fakeCondition === false ) { x--; } }; for ( var x = 0; x < 5; log(), x++ ) { console.info( x ); } |
Este fragmento entra en un bucle infinito ya que tras cada iteración, la variable x no cambia y, por tanto, no se cumple la condición de salida.
NOTA: Si alteramos el orden de la llamada a log() y la ponemos detrás del incremento de x, llegaremos a la función con el valor ya incrementado:
for ( var x = 0; x < 5; x++, log() ) { console.info( x ); } |
Ampliando el operador ternario
Tradicionalmente, siempre he defendido el operador ternario como una construcción elegante y muy legible. En su día, le dediqué una entrada al modelo largo que podéis re visitar en este enlace.
En el modelo tradicional, una de las limitaciones de este operador es que solo permite una única instrucción/expresión para cada condición dada:
condition ? instructionIfTrue : instructionIfFalse; |
Pero como gracias al operador coma podemos encadenar expresiones, nuestro ternario puede también beneficiarse de múltiples instrucciones:
var playerLost = () => { lives ? ( lives--, continue() ) : ( gameOver(), exitGame() ); } |
Pese a que es una construcción que fácilmente puede recrearse con un simple condicional, cuando necesitamos minimizar nuestro código puede resultar práctico. La versión con ‘if‘ quedaría como sigue:
var playerLost = () => { if ( lives ) { lives--; continue(); } else { gameOver(); exitGame(); } } |
¿Más simple y legible? Sí; pero también más larga y no tan bonito.
Evaluación perezosa o por cortocircuito avanzada
Aquí, de nuevo, estamos utilizando el hecho de que el operador coma ejecuta instrucciones en un orden estricto para, aprovechando esa funcionalidad, evaluar por cortocircuito un siguiente elemento de la cadena.
Veamos un ejemplo con una función que devuelve el primer ancestro encontrado en el DOM a partir de un selector y un tipo de nodo:
var firstAncestor = ( el, tagName ) => { while( el = el.parentNode, el && ( el.tagName != tagName.toUpperCase() ) ); return el; } // element in http://www.etnassoft.com/ var el = document.getElementById( 'back_to_top_link' ); firstAncestor( el, 'div' ); //<div id="subfooter_content"> |
Aquí la cuestión es fijarse en el uso de la evaluación perezosa dentro del bucle while:
while( el = el.parentNode, el && ( el.tagName != tagName.toUpperCase() ) ); |
Si la evaluación de la primera expresión ‘el = el.parentNode‘ devuelve un nodo, la condición ‘el && …‘ se cumplirá (ya que no tendríamos un Falsy Values en Javascript). Eso permite al intérprete continuar computando el paréntesis para, finalmente, devolver el último valor obtenido: un true o un false (según se cumpla la condición !=) que indica al bucle si continuar o no iterando.
Usos elegantes
Bueno; habíamos visto hasta ahora teoría, fragmentos poco intuitivos y pseudo código, pero el último ejemplo abría la puerta a unas estructuras más interesantes que pueden llamarnos la atención por su elegancia y claridad (aunque claridad y legibilidad no sean aquí sinónimos). Pasemos ahora a ver otras piezas elegantes realizadas en base a este operador.
NOTA: La mayoría de ejemplos que ilustran esta entrada son autoria del genio Angus Croll, un grande del lenguaje Javascript que, incomprensiblemente, no es tan reconocido a nivel mundial como otros profesionales más mediáticos pero mucho menos experimentados y creativos… Porca miseria.
Incremento y decremento de dos contadores al mismo tiempo
var renderCurve = function () { for ( var a = 1, b = 10; a * b; a++, b-- ) { console.log( new Array( a * b ).join( '*' ) ); } } renderCurve(); /* ********* ***************** *********************** *************************** ***************************** ***************************** *************************** *********************** ***************** ********* */ |
Secuencia de Fibonacci
No podía faltar este clásico de la algoritmia como demostración práctica de lo que sea.
var fibonacciGen = function ( n ) { for ( var i = 2, r = [ 0, 1 ]; i < n; r.push( r[ i - 1 ] + r[ i - 2 ] ), i++ ); return r; } console.info( fibonacciGen( 15 ) ); // [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377] |
La coma para separar variables con ‘var’
Llegados a este punto es interesante recalcar un caso habitual de uso de la coma en Javascript que, pese a lo que puede parecer, no es el operador del que estamos tratando. Nos referimos al separador cuando declaramos variables de forma encadenada:
var foo, bar, foobar; |
Esa coma es parte de la sintaxis de la instrucción ‘var’. Recordemos que, según la especificación, esta sería:
var varname1 [= value1 [, varname2 [= value2 ...[, varnameN [= valueN ']]]]]; |
No podemos hablar aquí de operador porque no estamos trabajando en una expresión.
Conclusión
El operador coma es una herramienta más del lenguaje que nos permite encadenar expresiones bajo una misma instrucción retornando el último valor computado. Su estructura y flexibilidad permite escribir códigos más pequeños aunque ligeramente más complejos de seguir a simple vista. Es por ello que podemos verlo con cierta frecuencia en programas minificados o procesados por algún ‘transpiler‘ como por ejemplo Babel.
Como ejercicio práctico, hemos puesto algunos ejemplos creativos que hacen uso de este operador quedando como siempre a criterio de cada programador si su sintaxis y estructura se acomoda a sus habilidades.
El ejemplo de «Dividir una cadena en arrays de n caracteres de longitud» (splitStr) no usa el operador coma en ningún sitio 🙂
El ejemplo de “Dividir una cadena en arrays de n caracteres de longitud” (splitStr) no usa el operador coma en ningún sitio.
Mis comentarios no están siendo aprobados por algún motivo que desconozco, pero insisto en que el ejemplo de splitStr no usa el operador coma en ningún sitio. Creo que sería bueno revisarlo.
Mis disculpas! El WP había filtrado tus comentarios y no se estaban publicando; ¡corregido!
Con respecto a tu comentario, tienes toda la razón… En ese ejemplo no aparece el operador del que hablamos. El motivo es que, en algún momento, reformateé la función y en ese cambio suprimí el operador por un Array.map. Como el fragmento es en cierta medida confuso, aprovecho para eliminarlo.
Gracias por la observación!