Tabla de Contenidos

Desestructuración

Módulo perteneciente al curso Programación avanzada con JavaScript y ECMAScript.

Interpolación de cadenas

La interpolación de cadenas es una pequeña novedad de ECMAScript 2015, que a pesar de ser syntactic sugar puro, seguro que se convertirá en una de tus novedades favoritas.

Actualmente en JavaScript podemos crear cadenas de dos formas, ya sea con comillas dobles (") o con las comillas simples ('). Por si alguna vez te lo has preguntado, no hay diferencia alguna entre usar comillas dobles o simples. La razón por la que existan esas dos opciones de caracteres para delimitar las cadenas es porque de ese modo evitamos tener que “escaparlos” si queremos utilizarlos dentro de una cadena:

var str  = "Sayonara 'baby'";
var str2 = 'Sayonara \'baby\'';
var str3 = 'Hello "amigo"';
var str4 = "Hello \"amigo\"";

Como se puede observar las versiones que no requieren escapar ningún carácter, son más legibles. Esa es la razón principal por la cual existen dos delimitadores distintos para una cadena: elige el que prefieras y usa el otro dentro.

Si alguna vez construyes JSON de forma manual (aunque no deberías hacerlo nunca, sino que deberías usar JSON.stringify en su lugar) recuerda que las cadenas en JSON siempre son con comillas dobles.

Interpolando cadenas

Bajo el nombre de “interpolación” de cadenas no se esconde nada más que un mecanismo que te permite pasar una cadena que contiene un conjunto de comodines o placeholders junto con unos datos. El resultado es la misma cadena pero sustituyendo cada uno de los comodines por el dato concreto.

Veamos cómo podríamos hacer eso en ECMAScript 5:

String.prototype.format = function() {
    var args = arguments;
    return this.replace(/{(\d+)}/g, function(match, number) { 
        return typeof args[number] != 'undefined'
        ? args[number]
        : match
        ;
    });
};

Este código añade al String.prototype (prototipo de todas las cadenas) la función format. Esta función usa una expresión regular para sustituir los placeholders en forma {0}, {1} y así sucesivamente, con el argumento correspondiente al orden indicado:

var str = "{0}{1} mola mucho, pero {0} {2} todavía mola más".format("ECMASCript","5","2015");

El valor de str es: "ECMASCript5 mola mucho, pero ECMASCript 2015 todavía mola más".

Tener un método para interpolar cadenas nos evita tener que usar concatenaciones que al final terminan ensuciando mucho el código.

Pues bien en ECMAScript 2015 las tenemos de serie, en el lenguaje. Sin necesidad de invocar a función alguna y ¡encima con más potencia! Observa el siguiente código:

var a0 = "ECMASCript";
var a1 = 5;
var a2 = 2015;
var str=`${a0} ${a1} mola mucho, pero ${a0} ${a2} todavía mola más`;

Este código deja en str el mismo valor que antes.

Fijémonos en los detalles:

Veamos otro código que deje claro la evaluación de expresiones dentro de los placeholders:

var a = 10;
var b = 32;
var str = `El resultado de sumar ${a} y ${b} es ${a+b}`;

En resumen, la interpolación de cadenas nos permite crear fácilmente cadenas compuestas por varios elementos de forma sencilla y mucho más legible que usando concatenaciones. Ya verás que cuando empieces a usarla… ¡te costará volver a concatenar cadenas!

Desestructuración en arrays

La desestructuración (en inglés destructuring) es una nueva característica en ECMAScript 2015 que permite extraer fácilmente elementos de arrays y objetos y asignarlos a variables.

La desestructuración está basada en el pattern matching, una característica típica de los lenguajes funcionales.

Veamos un ejemplo de su uso:

var [a,b] = [1,2];

Este código deja el valor 1 en la variable a y el valor 2 en la variable b.

Sin la desestructuración, para extraer elementos de un array y asignarlos a variables usábamos habitualmente la asignación con índice. Con la desestructuración, en una sola línea podemos extraer los elementos que deseemos.

var data = [4, 8, 15, 16, 23, 42];
 
// Sin desestructuración
var first = data[0];
var second = data[1];
var fourth = data[3];
 
// Con desestructuración
var [first, second,,fourth] = data;

Observa el código de la última línea en el ejemplo anterior. Las dos comas seguidas. Eso permite saltarse un elemento del array que estamos desestructurando. Es decir, first será el primer elemento del array data, second será el segundo, el tercero queda sin asignar (por las dos comas seguidas) y fourth pasa a ser el cuarto elemento de data.

La desestructuración no requiere que haya constantes en el lado derecho de la asignación, funciona con variables también:

var x=10;
var y=20;
var z=30;
var [x,y,z] = [z,x,20];

Al final de este código el valor de:

Pregunta: ¿Cómo intercambiarías el valor de dos variables usando la desestructuración?

Desestructuración en valores de retorno

Si una función devuelve un array, se puede usar la desestructuración para obtener todos o aquellos elementos del array que nos interesen:

var foo = function() {return [1,2,3];}
var [a,,b] = foo();

Al final de este código, a vale 1 y b vale 3 (observa las dos comas seguidas).

La desestructuración es segura. Si intentamos desestructurar más elementos de los que hay en el array, los que no existan devuelven undefined.

DEMO: Desestructuración en arrays

La desestructuración se basa en pattern matching.

var arr = [10, 20];
 
// Lo que nos permite la desestructuración es declarar variables de la siguiente manera
var [a, b] = arr;
 
a; // -> 10
b: // -> 20
 
var [c,d] = [30, 40];
 
c; // -> 30
d; // -> 40

Esta característica también nos permite intercambiar valores de variables en una única instrucción:

[a, c] = [c, a];

Incluso podemos decidir qué valores recoger. En el siguiente ejemplo cogeremos el primer y último elementos:

arr = [10, 20, 30, 40];
 
var [i,,,f] = arr;
 
i; // -> 10
f; // -> 40

Incluso podemos hacerlo con arrays más pequeños de lo esperado y no nos dará error:

var [e,,,f] = [10];
 
e; // -> 10
f; // -> 'undefined'

Desestructuración en objetos

La desestructuración también funciona en objetos. Es decir, se puede desestructurar un objeto y asignar todas o algunas de sus propiedades a variables simples:

var obj = {a: 10, b: 42};
// Sin desestructuración
var x = obj.a;
var y = obj.b;
// Usando desestructuración
var {a: x, b: y}  = obj;

Al finalizar el código el valor de x es 10 (el valor de la propiedad a del objeto obj). Y el valor de y es 42.

Podemos desestructurar parcialmente un objeto, no necesitamos desestructurar todas sus propiedades:

var {b: x} = {a: 10, b: 42}

Al finalizar el código la variable x vale 42.

Por supuesto, puede combinarse la desestructuración de arrays y objetos:

var data = {a:10, b:[1,2,3, {c: 40, d: 50}]};
var  {b: [,x]} = data;

Al finalizar este código el valor de x es 2, que es el segundo elemento del array data.b. Como puedes ver el poder de la desestructuración es enorme.

Desestructuración en parámetros

La gestión de parámetros opcionales en las funciones en JavaScript siempre ha sido problemática. Cierto es que usando typeof y el operador || se puede conseguir mucha flexibilidad, pero el principal problema sigue siendo que viendo la firma de la función uno no sabe qué parámetros pasarle. Mejor veámoslo con un ejemplo:

var foo = function(params) {
    var uri = params.uri;
    var method = params.method || 'GET';
    var contentType = params.contentType || 'application/json';
    // Hacer la petición a la uri indicada con el método y content-type indicados
}
// Uso de la función:
foo({uri: 'http://campusmvp.es'});  // method es GET y contentType es 'application/json'
foo({uri: 'http://campusmpv.es', contentType: 'text/html', method: 'GET'});

El principal problema para alguien que quiera llamar a foo es ¿cómo sabe cuáles son las propiedades del objeto que se le debe pasar como parámetro?

No hay más solución que leerse todo el código fuente de la función. Solo así podemos ver que la función foo espera las propiedades uri, method y contentType dentro del objeto. Usar la desestructuración ayuda a explicitar este hecho:

var foo = function({uri: uri, method: method, contentType: contentType}) {
    // En este punto las variables uri, method y contentType ya existen gracias a la desestructuración
}

Para quien llame a foo no hay diferencia entre ambas versiones, pero la segunda deja mucho más claras las propiedades que se esperan en el objeto que se recibe como parámetro. En este ejemplo el nombre de la propiedad (a la izquierda de los dos puntos) y el de la variable local generada por la desestructuración (a la derecha de los dos puntos) son iguales, aunque no tiene por qué. Pero si este es el caso todavía podemos simplificar más el código:

var foo = function({uri, method, contentType}) {
    // En este punto las variables uri, method y contentType ya existen gracias a la desestructuración
}

Recuerda que en ECMAScript 2015 si usamos notación de objeto y una propiedad “x” se inicializa a partir de una variable llamada “x” no tenemos por qué poner {x:x}, sino que podemos poner simplemente {x}.

Incluso podemos ir un paso más allá y dar valores por defecto a las propiedades no pasadas:

var foo = function({uri, method: method='GET', contentType: ctype='application/json'}) {
    // En este punto las variables uri, method y ctype ya existen gracias a la desestructuración
}

En este caso si el llamante no especifica el valor de las propiedades method y contentType éstas toman los valores por defecto.

Observa que cuando usamos los valores por defecto, aunque la variable resultado de la desestructuración se llame igual que la propiedad del objeto, debemos especificar ambos (caso de method en el ejemplo anterior).

DEMO: Desestructuración en objetos

var obj = {
  a: 10,
  b: 42
};

Sin desestructuración:

var x = obj.a;
var y = obj.b;

Con la desestructuración:

var {a: x, b: y} = obj;

Como vemos, ponemos primero el nombre de la propiedad que queremos desestructurar y luego la variable que recibirá el valor de la desestructuración.

console.log(x, y); // -> 10 42

Podemos desestructurar parte del objeto:

var { b: z } = obj;
 
console.log(z); // -> 42

Desestructurando propiedades que no existen:

var {a: x, _b: y} = obj;
 
console.log(x, y); // 10 'undefined'

En resumen, la desestructuración nos permite extraer elementos de un array o valores de propiedades de un objeto a variables simples. Y extraer todos y algunos elementos. Si no existen las variables a desestructurar, se asignará undefined.

DEMO: Desestructuración en parámetros

Es muy habitual en JavaScript hacer funciones que acepten un objeto como parámetro. En la firma de la función no sabemos qué propiedades tendrá ese objeto:

var f = function(params) {
    console.log(params.url);
}
 
f({url: "campusmvp.es"});

Con la desestructuración:

var f = function({url: x}) {
    console.log(x);
}
 
f({url: "campusmvp.es"});

Esto permite tener parámetros nombrados dentro de un objeto y ayudar a la legibilidad del código.

Operador spread

El operador spread permite que en situaciones en las que se espera un valor, aparezcan varios valores. Es un operador muy potente, y la mejor manera de entenderlo es verlo en acción.

Empecemos viéndolo en combinación con la desestructuración:

var [a,b,...c] = [10, 20, 30, 40, 50];

El operador spread son los tres puntos (). En este caso estamos aplicando el operador a la variable c. Por la desestructuración la variable c tomaría el tercer valor del array, pero gracias al operador spread puede tomar más de uno. El resultado es que la variable c termina siendo un array con el resto de valores del array desestructurado. Es decir, la variable c pasa a ser un array con los valores 30, 40 y 50.

Cuando el operador spread se usa de ese modo, tan solo puede aparecer una vez y en el último lugar.

Otro uso interesante del operador es llamar a funciones con N parámetros a partir de un array con N valores:

var foo = function(a,b) {
  console.log(a,b);
}
foo(...[10,20]);

Observa cómo llamamos a la función foo y le pasamos como parámetro el operador spread aplicado al array [10, 20]. Al aplicar el operador spread de esa forma el primer valor del array pasa al primer parámetro de la función, el segundo valor del array al segundo parámetro y así sucesivamente. Eso nos permite invocar a funciones con varios parámetros, cuando tenemos esos parámetros en un array sin necesidad de usar apply.

Se pueden combinar varios operadores spread llamando a una función, y eso se puede combinar con parámetros normales. La flexibilidad es total:

var foo = function(a,b,c,d,e) {
}
foo(10, ...[20, 30], 40, ...[50]);

En este código…

Parámetros rest

El operador spread se puede aplicar al último parámetro de una función. Cuando hacemos esto decimos que este parámetro es un “parámetro rest”. El valor de un parámetro rest es un array con todos los valores que se hayan pasado a la función y que no estén en ningún parámetro nombrado (convencional). Solo puede haber un parámetro rest en una función y debe ser el último:

var foo = function(a,b, ...c) {}
foo(10, 20, 30);        // c vale [30]
foo(10, 20, 30, 40);    // c vale [30, 40]
foo(10,20);             // c vale [] (es decir, un array vacío, no undefined)

Quizá te preguntes qué aportan los parámetros rest, existiendo arguments. Pues dos cosas importantes. La primera es que un parámetro rest solo tiene los valores que no están en ningún parámetro nombrado (c solo contiene los valores adicionales que no están en a ni en b), mientras que arguments contiene siempre todos los parámetros pasados. Pero la diferencia más importante es que arguments no es un array. Se le parece (es lo que llamamos un array-like object) pero no lo es. Y eso implica que no podemos usar todas las funciones de Array.prototype en arguments. Mientras que un parámetro rest sí que es un array de verdad.

DEMO: Operador spread

Desestructuración normal:

var arr = [1,2,3,4,5,6,7];
 
var [x,,,y] = arr;
 
console.log(x,y); // -> 1 4

Aplicando el operador spread ():

var arr = [1,2,3,4,5,6,7];
 
var [x,,,y,...z] = arr;
 
console.log(x,y); // -> 1 4 Array [ 5, 6, 7 ]

El operador spread permite que varios valores se obtengan en un nuevo array.

Este operador también nos permite combinar fácilmente arrays:

var uno = [1, 2, 5, 6];
 
// Queremos que el segundo array tenga unos elementos y el anterior array:
var dos = [100, 200, ...uno, 300];
 
dos;// Array [ 100, 200, 1, 2, 5, 6, 300 ]

Otro de los usos de este operador es para las funciones sin necesidad de usar apply:

var foo = function(a, b, c, d, e) {
    console.log(a, b, c, d, e);
}

Uso:

var arr = [1,2,3,4,5];
 
// Imaginemos que queremos asociar cada uno de esos valores del array
// a los distintos argumentos de la función 'foo'. Con spread es así de fácil:
foo(...arr);

Si tuviéramos menos elementos:

var arr = [1,2,3,4,5];
 
// El resto de parámetros de la función se los podríamos
// pasar así:
foo(...arr, 100, 200);

También es posible hacer esto:

var arr = [1,2,3,4,5];
var arr2 = [200, 300];
 
 
foo(...arr, ...arr2);
 
También podemos hacer:
 
foo(...arr, ...[10], ...[30]);

DEMO: Parámetros rest

Llamamos parámetros rest a que podemos aplicar el operador spread como último parámetro de una función y este último parámetro será un array que contendrá todos los parámetros no nombrados de la función.

var foo = function(a, b, ...c) {
    console.log(a);
    console.log(b);    
    console.log(c);   
}
 
foo(10, 20, 30); 
// 10
// 20
// Array [ 30 ]
 
foo(10, 20, 30, 40, 50, 60);
// 10
// 20
// Array [ 30, 40, 50, 60 ]

Los parámetros rest no fallan, por ejemplo, veamos qué pasa si le pasamos a la función todos los argumentos menos los rest:

foo(10, 20);
 
// 10
// 20
// Array [ ]

Vamos a ver diferencias con arguments:

var foo = function(a, b, ...c) {
    console.log(a);
    console.log(b);    
    console.log(c);   
    console.log(arguments);
}

Usamos:

foo(10, 20, 30, 40); 
// 10
// 20
// Array [ 30, 40 ]
// Arguments { , 7 more... }

arguments contiene todos los parámetros, ya sean nombrados o no. rest solo contiene los nombrados. El parámetro rest es un array, arguments no es un array, así que no podemos usar operaciones típicas de arrays (map, filter).