El Operador de Propagación en Javascript (ECMAScript 6 y polyfill)

03 Jun 2014

Introducción

En el artículo anterior pudimos ver cómo los ‘rest parameter‘ nos permiten manejar un número indeterminado de argumentos en nuestras funciones. Pero podemos utilizar este mismo concepto para, en modo inverso, convertir un array en una serie de argumentos para una función. Para ello, ECMAScript 6 incorpora el Operador de Propagación, el cual permite exactamente eso.

Veamos cómo utilizarlo…

Ejemplo básico

El Operador de Propagación, o spread operator, se compone del nombre de nuestro array precedido por tres puntos:

var foo = [ 'En', 'un', 'lugar', 'de', 'la', 'Mancha' ];
 
console.info( ...foo );
// En un lugar de la Mancha

En este sencillo ejemplo, estamos cogiendo nuestro array para procesar cada uno de sus elementos como argumentos de la propia función ‘console.info’, por lo que el resultado sería equivalente a escribir:

console.info( foo[ 0 ], foo[ 1 ], foo[ 2 ], foo[ 3 ], foo[ 4 ], foo[ 5 ] );

La idea es la expuesta: descomponemos el array en elementos y pasamos cada uno de ellos como argumentos a una función. La comodidad aquí es que no necesitamos conocer el número de elementos que contiene el array ni crear bucles que lo recorran, o usar otras funcionalidades del objeto Array.

Concatenar Arrays

Esta funcionalidad del operador de propagación puede ser especialmente útil por ejemplo para concatenar arrays. Por ejemplo:

var foo = [ 'En', 'un', 'lugar', 'de', 'la', 'Mancha' ],
    bar = [ 'de', 'cuyo', 'nombre', 'no', 'quiero', 'acordarme' ],
 
    // Old Style
    oldStyle = foo.concat( bar ),
 
    // ECMAScript 6 style
    ES6Style = [ ...foo, ...bar ];
 
console.info( oldStyle );
// [ "En", "un", "lugar", "de", "la", "Mancha", "de", 
    "cuyo", "nombre", "no", "quiero", "acordarme" ]
 
console.info( ES6Style );
// [ "En", "un", "lugar", "de", "la", "Mancha", "de", 
    "cuyo", "nombre", "no", "quiero", "acordarme" ]

Como vemos, hemos utilizado las dos formas: la clásica del concat, y el método más ‘reciente’ (y más legible) que nos permite ES6.

El funcionamiento en este caso es similar: se han cogido cada uno de los elementos de los arrays que se quieren procesar para pasarlos como argumentos de un tercero. El resultado lógico es que obtenemos una nueva matriz con cada uno de los elementos independientes de sus dos fuentes. Simple y elegante!

Uso simple en funciones

Si antes vimos el ejemplo sobre una función ‘predefinida’ como es console, podemos extrapolar el ejemplo a una función personalizada. Pensemos por ejemplo en una función que espera una serie de parámetros para guardar un objeto en una base de datos:

var saveBook = function ( title, author, publisher ) {
    /* Awesome logic goes here... */
    console.info( 
        'The book ', title, 
        ' by ', author, 
        ' published by ', publisher, 
        ' has been added to database!' 
    );
}
 
var book1 = [ 'JavaScript: The Good Parts', 'Douglas Crockford', 'OReilly' ],
    book2 = [ 'JavaScript: The Definitive Guide', 'David Flanagan', 'OReilly' ];
 
saveBook( ...book1 );
// The book JavaScript: The Good Parts by Douglas Crockford 
// published by OReilly has been added to database!
 
saveBook( ...book2 );
// The book JavaScript: The Definitive Guide by David Flanagan
// published by OReilly has been added to database!

El ejemplo juega con este operador de propagación para tomar una matriz (un array) y convertirlo en argumentos independientes para nuestra función saveBook.

Si pasamos más elementos en el array de los que espera la función, no ocurre ningún error:

var book3 = [ 'Eloquent JavaScript', 'Marijn Haverbeke', 
    'No Starch Press', '2011', 'English' ];
 
saveBook( ...book3 );
// The book Eloquent JavaScript by Marijn Haverbeke 
// published by No Starch Press has been added to database!

Si por el contrario omitimos valores, el intérprete Javascript los reemplazará por ‘undefined’, pero no lanzará un mensaje de error:

var book4 = [ 'JavaScript Patterns' ];
 
saveBook( ...book4 );
// The book JavaScript Patterns by undefined 
// published by undefined has been added to database!

Este último ejemplo, que podría provocar errores o incosistencias, podría quedar mucho más completo si añadimos a nuestros argumentos un valor por defecto tal y como vimos en el post Cómo asignar valores por defecto a los argumentos de una función (revisión ES6):

var saveBook = function ( title = 'Unknown', author = 'Unknown', publisher = 'Unknown' ) {
    /* Awesome logic goes here... */
    console.info( 
        'The book ', title, 
        ' by ', author, 
        ' published by ', publisher, 
        ' has been added to database!' 
    );
}

Por lo que ahora, si omitimos argumentos, obtenemos al menos un valor por defecto en lugar del problemático ‘undefined’:

var newBook = [ 'Node.js in Action' ];
 
saveBook( ...newBook );
// The book Node.js in Action by Unknown published by Unknown has been added to database!

¿Y si el objeto que pasamos no es iterable?

En ese caso, el intérprete nos dará un error interrumpiendo la ejecución de nuestro programa:

var foo = { foo: 'Hello World', bar: 'Goodbye Lenin' };
 
console.info( ...foo );
// TypeError: foo is not iterable

Para evitar que un error de este tipo pueda bloquear nuestra aplicación, habría quizá que recurrir a algún tipo de comprobación previa, o a coercionar el tipo de la variable sobre la que queremos operar… Por ejemplo:

var foo = { foo: 'Hello World', bar: 'Goodbye Lenin' },
    bar = [ 'La', 'donna', 'e', 'mobile' ];
 
// Checking
foo.length && ( console.info( ...foo ) ); // Nothing happens
bar.length && ( console.info( ...bar ) ); // La donna e mobile
 
// Coercing
foo.length || ( foo = [] );
console.info( ...foo ); // (nothing to show)

No son soluciones a priori elegantes, pero evitamos así el error: comprobamos si la variable es ‘iterable’ preguntando por su atributo ‘length’; en caso de que no lo posea, o bien no llamamos a nuestra función (primer método del ejemplo), o bien reescribimos su valor por un array vacío (segundo método).

¿Operador de Propagación en ambos lados?

Si hemos visto que este nuevo operador se puede usar tanto en los argumentos de las funciones como en las llamadas, ¿es posible usarlo en ambos lados al mismo tiempo? Posible es: no tiene ninguna utilidad aparente, pero se puede.

Solo a modo de ejemplo:

var findMaxValueIn = function ( ...numbers ) {
    return Math.max.apply( Math, numbers );
};
 
var values = [ 20, 40, 10, 30 ];
 
console.info( findMaxValueIn( ...values ) ); // 40

El sinsentido del ejemplo es que convertimos un array en valores individuales para luego volverlos a recomponer. Lo cierto es que no encuentro ningún escenario donde esto podría ser interesante, pero aquí queda como demostración de la flexibilidad del lenguaje 🙂

Polyfill

Como viene siendo habitual, para aquellos navegadores antiguos que no soporten la nueva especificación, hay que modificar este operador de propagación por su forma extendida. En este caso, sin embargo, es más sencillo que con funcionalidades más complejas. Basta modificar la llamada simple a nuestra a nuestra función y recurrir al método apply. Volviendo a nuestro ejemplo anterior de saveBook, basta con:

saveBook.apply( null, book );

Resulta menos intuitivo y legible, pero nos puede sacar de un apuro si el navegador no soporta la nueva sintaxis.

Conclusión

El operador de propagación puede ser una herramienta interesante, y muy potente, para trabajar con funciones que reciben parámetros desde fuentes externas. Puede ser por ejemplo muy útil cuando trabajamos con la respuesta que nos proporciona una API de terceros, o para manipular los datos que recogemos de un formulario. Usado de forma más exótica, también nos permite concatenar arrays…

Como siempre se dice por aquí, cualquier innovación en el lenguaje es bienvenida, y esta no va a ser menos!

Más:

{2} Comentarios.

  1. franklin

    Hola, que navegadores soportan esta nueva implementacion?

    • Carlos Benítez

      Hola!
      Pues en principio todos los navegadores actuales basados tanto en SpiderMonkey (Firefox) como en WebKit (Chrome, Safari, Opera, …).

      Si revisamos la tabla de compatibilidad del ES6,
      http://kangax.github.io/compat-table/es6/

      parece que IE no lo soporta, por lo que ahí tendríamos que usar los polyfills.

      Saludos!

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *