Desestructurando datos

Figura
Figura. El destructuring de un Array en ES6.

El desestructurado de datos (destructuring) consiste en extraer datos individuales desde una estructura de datos usando sólo una sentencia. En ES6 puede aplicarse sólo a objetos (Object) y Arrays. Para entenderlo mejor empecemos por ver como estructuramos datos en JavaScript. Supongamos que tenemos tres variables a = 1, b = 2, c = 3 y queremos construir un Array arr cuyos items sean los valores de aquellas variables:

//Estructurando un Array    
let a = 1, b = 2, c = 3;
let arr = new Array();
arr[0] = a; arr[1] = b; arr[2] = c;
console.log(arr); // [1, 2, 3]
    

Con notación literal con corchetes [ ] lo anterior sería más simple e intuitivo:

//Estructurando un Array con notación literal
let a = 1, b = 2, c = 3;
let arr = [a, b, c];
console.log(arr); // [1, 2, 3]
    

Hemos construido una estructura de datos (un Array) a partir de datos individuales. Ahora podemos manejar la variable arr en lugar de las variables individuales. En esa estructura podemos acceder a sus datos individuales por su posición, así arr[i] es el dato en la posición i-ésima. Podemos denominar este proceso como estructurar datos.

¿Cuál sería el proceso inverso? Se trata de partir de una estructura de datos y obtener datos individuales. Esta operación podría denominarse desestructurar, desmontar o desarmar una estructura de datos. Con el mismo ejemplo partiríamos de un Array y lo desmontamos en tantas variables individuales como posiciones tenga el Array, tal como se representa esquemáticamente en la Figura. Con lo que sabemos de JavaScript hasta ahora haríamos lo siguiente:

//Desestructurando un Array
let arr = [1, 2, 3];
let a = arr[0], b = arr[1], c = arr[2];
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
    

Ese código es una inversión del primer codigo. Si antes estructuramos, ahora desestructuramos. Antes podíamos agrupar varias sentencias mediante notación literal para construir un Array con una única sentencia. Ahora también sería útil hacer el desestructurado en un única sentencia en lugar usar una para cada item del Array. Y eso es lo que pretende la técnica destructuring de ES6. Observe como este código es un especie de inversión del segundo que usa notación literal:

//Destructuring de un Array
let arr = [1, 2, 3];
let [a, b, c] = arr;
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
    

Podemos hacer lo mismo para objetos como estructuras de datos. Con este código estructuramos un objeto a partir de datos individuales también usando notación literal con llaves { }:

//Estructurando un objeto
let a = 1, b = 2, c = 3;
let obj = {a: a, b: b, c: c};
console.log(obj); // {a: 1, b: 2, c: 3}
    

Y con el siguiente lo desestructuramos:

//Destructuring de un objeto
let obj = {a: 1, b: 2, c: 3}
let {a: a, b: b, c: c} = obj;
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
    

En los siguientes apartados veremos más detalles relacionados con la sintaxis tanto para Arrays como para objetos.

Sintaxis del destructuring para Arrays

Figura
Figura. El destructuring de un Array en ES6.

La sintaxis básica consiste en las dos partes de una expresión de asignación. En la parte izquierda encontramos el patrón del desestructurado. En el siguiente código es [a, b, c], pues esto le sirve a JavaScript para entender como debe recuperar los valores en la parte derecha de la asignación. Esa parte derecha es el inicializador de las variables.

//patrón = inicializador
let [a, b, c] = [1, 2, 3];
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
    

Con el desestructurado siempre tiene que haber un inicializador. Para las declaraciones de variables podemos omitir la parte derecha o inicializador con var y let, pues en ese caso JavaScript inicializa las variables con el valor undefined. Pero no para const que siempre necesita un inicializador:

var a; // Sin error: inicializa a undefined
let b; // Sin error: inicializa a undefined
const c; // SyntaxError: Missing initializer in const declaration
    

Al igual que para const al declarar constantes, en destructuring siempre tiene que haber un inicializador sea cual sea la declaración var, let o const usada:

//Cualquiera de las tres sentencias siguientes
//es un error de sintaxis: 'Missing initializer
//in destructuring declaration'
var [a, b]; 
let [c, d];
const [e, f];
    

No es necesario anteponer var, let o const antes del patrón si previamente declaramos las variables:

//Podemos declarar todas las
//variables previamente...
let a, b, c, d;
//y después inicializarlas
[a, d] = [1, 2];
[b, c] = ["x", null];
console.log(a); // 1
console.log(b); // "x"
console.log(c); // null
console.log(d); // 2
    

Es más, si no estamos en modo estricto podríamos incluso omitir las declaraciones, pues en ese caso se presupone var y las variables se crearán en el espacio global. Pero cuidado si estamos en modo estricto, pues siempre es necesario usar alguna declaración var, let o const

//No estricto
[a, b] = [1, 2];
console.log(a); // 1
console.log(b); // 2
//Modo estricto
(function(){
    "use strict";
    [x, y] = [1, 2]; // ReferenceError: x is not defined
    console.log(x);
    console.log(y);
})();    
    

Con const hay que tener especial cuidado pues a las constantes hay que darles un valor inicial y posteriormente no se pueden reasignar.

const a = 0, b = 0;
[a, b] = [1, 2]; // TypeError: Assignment to constant variable. 
console.log(a);
console.log(b);
    

Por lo tanto hay que aplicar el desestructurado y la declaración de constantes en una única sentencia:

const [a, b] = [1, 2];
console.log(a); // 1
console.log(b); // 2
    

El patrón no tiene porque abarcar todos los valores del inicializador. En el siguiente código extraemos los dos valores de la izquierda del Array. Pero también podemos extraer cualquier de ellos omitiendo el resto. Veáse como dejamos huecos por la izquierda en aquellos valores previos que no nos interese extraer.

let arr = [1, 2, 3, 4];
let [primero, segundo] = arr;
console.log(primero); // 1
console.log(segundo); // 2
let [ , , tercero] = arr;
console.log(tercero); // 3
    

Podemos usar valores por defecto en el desestructurado que se usarán si el valor en el inicializador es undefined o bien no aparece. En el siguiente código disponemos valores por defecto para todos los elementos del patrón. Para el primer elemento obtenemos a = "A". El segundo tiene el valor undefined y por tanto se usará su valor por defecto (b = 2). El valor nulo es un valor más y por tanto obtenemos c = null. Como el inicializador sólo tiene tres valores, para el cuarto elemento del patrón se usará su valor por defecto (d = 4).

let [a=1, b=2, c=3, d=4] = ["A", undefined, null];
console.log(a); // "A"
console.log(b); // 2
console.log(c); // null
console.log(d); // 4
    

Podemos usar el operador resto de elementos dentro del patrón:

let [a, ...b] = [1, 2, 3];
console.log(a); // 1
console.log(b); // [2, 3]
    

Sintaxis del destructuring para objetos

Figura
Figura. El destructuring de un objeto en ES6.

La sintaxis básica para desestructurar un objeto es usar un patrón con los nombres de las propiedades del objeto. Se crearán entonces variables con esos mismos nombres:

let {a, b} = {a: 1, b: 2};
console.log(a); // 1
console.log(b); // 2
    

Realmente el patrón {a, b} es un forma de simplificar {a: a, b: b}. Esta forma general además nos permite modificar los nombres de las variables, como en este ejemplo donde cambiamos el nombre de la variable aplicado a la segunda propiedad (ver Figura):

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

Podemos dar valores por defecto para los inicializadores undefined o que no aparezcan:

let {a:x=99, b=88} = {a: undefined};
console.log(x); // 99
console.log(b); // 88
    

Si declaramos previamente las variables, cuando escribamos la expresión de asignación hemos de envolverla en paréntesis, puesto que si una sentencia empieza con "{" JavaScript entendería que lo que sigue es un bloque de código.

let a, b;
//Rodear con paréntesis
({a, b} = {a: 1, b: 2});
console.log(a); // 1
console.log(b); // 2
    

Patrones complejos y errores de coincidencia

La forma en la que definimos el patrón debe ser consistente con la estructura de datos del inicializador. Si el patrón contiene variables para los que no se encuentra coincidencia en el inicializador, esas variables tendrán el valor undefined:

let [x, y, z] = [1, 2];
console.log(x); // 1
console.log(y); // 2
console.log(z); // undefined
    

El inicializador podría ser una estructura anidada compleja, pero según definamos el patrón se inicializarán las variables. En el siguiente código, la segunda variable portará el Array ["a", "b"] del inicializador:

let [x, y, z] = [1, ["a", "b"], 2];
console.log(x); // 1
console.log(y); // ["a", "b"]
console.log(z); // 2
    

Pero si definimos el patrón de otra forma las variables serán otras y se extraerán a otro nivel de profundidad:

let [x, [y1, y2], z] = [1, ["a", "b"], 2];
console.log(x); // 1
console.log(y1); // "a"
console.log(y2); // "b"
console.log(z); // 2
    

La estructura podría ser de cualquier grado de complejidad, pero lo importante es que se pueda producir la coincidencia. En el siguiente ejemplo se combinan Arrays y objetos:

let a, b, c, d;
[a, {x:b, y:[c, d]}] = [1, {x: 2, y: [3, 4]}]; 
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
console.log(d); // 4
     

Cuando no haya forma de hacer coincidir el patrón con el inicializador nos dará un error. En el siguiente código el patrón es [a, [b, c]] que no hay forma de hacerlo coincidir con [1, 2, 3]. Coincidiría si el inicializador hubiese sido algo como [1, [2, 3]].

let a, b, c;
try {
    [a, [b, c]] = [1, 2, 3]; 
} catch(e){
    console.log(e.message); // undefined is not a function
}
console.log(a); // 1
console.log(b); // undefined
console.log(c); // undefined
    

Veáse que encontró coincidencia en la pareja a vs 1 y de hecho inicializó esa primera variable con a = 1. Pero tuvo problemas con la siguiente pareja [b, c] vs 2. El mensaje de error no parece descriptivo del problema. De hecho Chrome 50 y Firefox 45 dan mensajes que en principio no nos indican que tenga que ver con el destructuring:

  • Chrome 50: Uncaught TypeError: undefined is not a function
  • Firefox 45: TypeError: [1, 2, 3][Symbol.iterator](...).next(...).value is not iterable

Lo bueno es que los errores de no coincidencia con el patrón son interceptables. Habrá que controlarlo si el inicializador proviene de algún otro proceso que pudiera dar una estructura de valores no coincidente con el patrón.

El inicializador no puede ser undefined o null pues no se podrá hacer coincidir el patrón con el inicializador. Con el siguiente código lo podemos comprobar:

try {
    let [a] = null; 
    console.log(a);
} catch(e){
    console.log(e.message); //Cannot match against 
                            //'undefined' or 'null'.
}
    

Entendiendo lo anterior, vemos en el siguiente código que haciendo coincidir el patrón anidado [a, [b, c]] con el inicializador [null, null] habrá coincidencia del primer elemento de cada parte, es decir, a frente al primer null. De hecho se realiza la asignación a = null. Pero ocurrirá un error al tratar de hacer coincidir [b, c] frente al segundo null.

let a, b, c;
try {
    [a, [b, c]] = [null, null]; 
} catch(e){
    console.log(e.message); //Cannot match against 
                            //'undefined' or 'null'.
}
console.log(a); // null
console.log(b); // undefined
console.log(c); // undefined
    

Observe entonces como este proceso de aplicar los patrones a los inicializadores se realiza de forma recursiva: si el elemento del patrón es a su vez un Array se espera que en el inicializador también exista un Array en esa posición. No puede haber null, ni undefined ni algo que no sea un Array. Podría ser un array-like, como arguments o resto de parámetros ...args. En el fondo se espera algo sobre lo que se pueda iterar con la finalidad de poder aplicar la coincidencia entre elementos:

(function(...args){
    let a, b, c, d, e;
    try {
        [a, [b, c], [d, e]] = [null, arguments, args]; 
    } catch(e){
        console.log(e.message); 
    }
    console.log(a); // null
    console.log(b); // 1
    console.log(c); // 2
    console.log(d); // 1
    console.log(e); // 2
})(1, 2);
    

Aunque aún no hemos visto las funciones generadoras, con estas podemos construir objetos iterables que podrían ser también inicializadores de una desestructuración. En el siguiente código la función generadora iterar() devuelve cada una de sus valores yield en cada llamada.

function* iterar(){    
    yield 1;
    yield 2;
    yield 3;
}
//El inicializador es un iterable
let [a, b, c] = iterar();
console.log(a); // 1
console.log(b); // 2
console.log(c); // 3
    

El desestructurado de parámetros

En todos los ejemplos que hemos visto en los apartados anteriores aplicábamos el desestructurado de datos en expresiones de asignación. Pero hay más sitios donde usar la desestructuración. Uno de ellos es en los parámetros de una función. En el siguiente ejemplo vemos el patrón del desestructurado de parámetros con un Array. Realmente la expresión de asignación está implícita en el momento de la llamada a la función, pues se llevaría a cabo [a, b] = [1, 2], donde indentificamos el patrón a la izquierda, en los parámetros, y el inicializador a la derecha, en los argumentos de la llamada.

function sumar([a, b]){
    return a+b;
}
console.log(sumar([1, 2])); // 3    
    

Podemos incluir valores por defecto en el patrón del desestructurado, lo que nos permitirá pasar un Array vacío o que le falte alguno de sus elementos. En la primera llamada del siguiente código tenemos la asignación implícita [a=0, b=0] = []. Cuando un elemento del patrón no aparece en el inicializador se le dará el valor por defecto.

function sumar([a=0, b=0]){
    return a+b;
}
console.log(sumar([])); // 0
console.log(sumar([1])); // 1  
console.log(sumar([,1])); // 1   
    

Si en el código anterior ejecutáramos sumar() nos saldría el error de que no pudo hacer coincidir el patrón contra undefined o null. La expresión de asignación implícita sería en ese caso [a=0, b=0] = undefined, que ya vimos en apartados anteriores que causa ese error. Para asegurar la ausencia total de argumentos tendríamos que usar parámetros por defecto en la función, lo que conseguimos agregando =[0, 0]:

function sumar([a=0, b=0] = [0, 0]){
    return a+b;
}
console.log(sumar()); // 0
    

Es evidente que con los parámetros podemos liarnos si vemos algo como fun([a=0, b=0] = [0, 0]). Es por lo que debemos recordar siempre que la expresión de asignación está implícita en la llamada a la función, momento en el que se asignan parámetros con valores de los argumentos. Y que esa igualdad en los parámetros no es una expresión de asignación sino la aplicación de parámetros por defecto, que nada tiene que ver con el desestructurado de datos.

El desestructurado de parámetros tiene una gran utilidad en el manejo de éstos. Los parámetros individuales son por naturaleza ordenados. Es decir, con la función constructora function Persona(nombre, profesion) hemos de llamarla con argumentos en el mismo orden que los parámetros. Para evitar eso se usan objetos como function Persona(obj), donde obj será un objeto como {nombre: "Luís", profesion: "piloto"}. Pero ese esquema no tiene implícitamente un control de valores, con lo que si pasamos el objeto {nombre: "Luís"}, en la propiedad de la profesión finalizaremos con un valor undefined. Podemos controlarlo con el desestructurado de parámetros:

function Persona({nombre="?", profesion="?"}={}){
    this.nombre = nombre;
    this.profesion = profesion;
}
//Podemos pasar las propiedades en cualquier orden
let luis = new Persona({profesion: "piloto", nombre: "Luís"});
console.log(luis); // Persona {nombre: "Luís", profesion: "piloto"}
//Sólo el nombre
let juan = new Persona({nombre: "Juan"});
console.log(juan); // Persona {nombre: "Juan", profesion: "?"}
//Sólo la profesión
let webmaster = new Persona({profesion: "webmaster"});
console.log(webmaster); // Persona {nombre: "?", profesion: "webmaster"}
//O sin argumentos
let desconocido = new Persona(); 
console.log(desconocido); // Persona {nombre: "?", profesion: "?"}
    

Veáse que el valor por defecto para los parámetros se indica con un objeto vacío ={}, de tal forma que cuando llamemos sin argumentos se asignará un objeto vacío y se rellenará con los valores por defecto del patrón de desestructurado.

Desestructurado en bucles for...of

Los bucles for...in nos permiten iterar sobre Array y objetos. En el siguiente código tenemos for (let prop in obj) de tal forma que en prop tendremos el nombre de la propiedad y en obj[prop] el valor. Para los Array el nombre de la propiedad es el índice (index) en el Array:

//for...in nos permite iterar sobre un Array
let arr = [1, 2];
for (let index in arr){
    console.log(`${index}: ${arr[index]}`); // 0: 1
                                            // 1: 2
}
//for...in nos permite iterar sobre un objeto
let obj = {a: 1, b: 2};
for (let prop in obj){
    console.log(`${prop}: ${obj[prop]}`); // a: 1
                                          // b: 2
}
    

Para evitar tener que acceder al valor con arr[index] se ha creado en ES6 el bucle for...of. Pondríamos entonces for (let item of [1, 2]) obteniendo en item el valor de cada posición. Sin embargo for...of no permite iterar sobre objetos, porque los objetos no son iterables. Entre los iterables están String, Array, Arguments, Map, Set y cualquier otra estructura que tenga esa condición de iterable.

//for...of nos permite iterar sobre un Array
for (let item of [1, 2]){
    console.log(item); // 1
                       // 2
}
//for...of nos permite iterar sobre un string
for (let car of "ab"){
    console.log(car); // "a"
                      // "b"
}
//for...of nos permite iterar sobre Arguments
(function(){
    for (let arg of arguments){
        console.log(arg); // 1
                          // 2
    }
})(1, 2);
//Pero for...of NO nos permite iterar sobre un objeto,
//porque un objeto no es iterable
try {
    for (let item of {a:1, b:2}){
        console.log(item);
    }
} catch(e){
    console.log(e.message); //Error, {a:1, b:2} no es iterable
}
    

Podemos construir un objeto iterarable con el nuevo built-in de ES6 Map. Se trata de un mapa que empareja claves y valores de la misma forma que lo hace un objeto. Pero ahora una mapa si es iterable. Una forma simple de construir un mapa es pasarle un Array compuesto a su vez de Arrays con parejas [clave, valor]. Y es en un bucle for...of para iterar sobre un mapa donde también podemos aplicar el desestructurado de las entradas del mapa.

//Un mapa son parejas clave-valor
let mapa = new Map([["a", 1], ["b", 2]]);
console.log(mapa); // Map {"a" => 1, "b" => 2} (ver NOTA)
//for...of nos permite iterar por el mapa
for (let item of mapa){
    console.log(item); // ["a", 1]
                       // ["b", 2]
}
//Desestructurando entradas del mapa
for (let [clave, valor] of mapa){
    console.log(clave, valor); // "a" 1
                               // "b" 2
}
    

Nota sobre representación de Mapas

Chrome 50 vierte un mapa en la consola como Map {"a" => 1, "b" => 2}, pero no debemos confundir las flechas "=>" como otra cosa que una correspondencia de parejas clave-valor. Firefox 45 lo representa con Map {a: 1, b: 2} que quizás es más adecuada. Pero no debemos olvidar que un mapa no es ni un Array ni un objeto, es un nueva estructura de datos.

Intercambio de variables con destructuring

En este apartado y los siguientes veremos algunos ejemplos de uso del desestructurado de datos. Una aplicación simple y a la vez útil es para intercambiar valores de variables. Sin destructuring tendríamos que usar una variable temporal para guardar uno de los valores:

let a = 1, b = 2;
let temp = a;
a = b;
b = temp;
    

El desestructurado nos facilita las cosas:

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

Ejemplo: Intercambiando variables con destructuring

Forma de intercambio
let a = 1, b = 2;
let temp = a;
a = b;
b = temp;
        
let a = 1, b = 2;
[a, b] = [b, a];
        
a = 1, b = 2
Este ejemplo usa ES6 en modo estricto. Puedes consultar el código JS original de este ejemplo.

Destructuring aplicado al resultado de expresiones regulares

Los métodos de expresiones regulares como match() o exec() devuelven un Array con las coincidencias encontradas. Es un ocasión óptima para aplicarles el desestructurado de datos. En este ejemplo aplicamos patrones para identificar las partes del código HTML de un único elemento. Usamos un patrón para separar el tag, atributos y contenido. Otro patrón nos permitirá separar los atributos.

let patronHtml = /<([a-z]+\w*)(\s+[^>]+)?\s*(?:\/>|>([^<]*)<\/\1>)/;
let patronAtrib = /([a-z]+[\w-]*)\s*(?:=\s*('[^']*'|"[^"]*"|\S+))?(?=\s*|$)/g;
    

A continuación aplicamos codigo.match(patronHtml) para obtener un Array cuya primera posición contiene la coincidencia con el patrón completo y el resto de posiciones son las coincidencias con los grupos de captura. Destructuramos ese Array aplicándolo a las variables tag, atrib (atributos) y conten (contenido):

let mensaje = "", tag = "", atrib = "", conten = "", html = "";
let codigo = document.getElementById("codigo-reg").value;
//Aplicamos el patrón para separar tag, atributos y contenido
let arr = codigo.match(patronHtml);
if (!arr) {
    mensaje = "No se encontró coincidencia con el patrón completo";
} else {
    //DESTRUCTURING del array de resultados
    [, tag = "", atrib = "", conten = ""] = arr;
    //Aplicamos el patrón de separar atributos
    if (atrib) {
        let bus = [], coin;
        while ((coin = patronAtrib.exec(atrib)) !== null) {
            bus.push(coin);
        }
        html = '<ul>';
        for (let item of bus){
            html += `<li><code>${item[1]}=${(item[2])?item[2]:""}</li>`;
        }
        html += '</ul>';
    }
}
//Volcamos tag, contenido y atributos en elementos de la página
    

Para separar los atributos usamos un bucle while que va aplicando patronAtrib.exec(atrib) y así componer un Array de dos dimensiones. Observe como el patrón ahora tiene el modificador g (global) activado. El Array obtenido con los atributos del código HTML inicial del ejemplo es el siguiente:

[['id="x y"', 'id', '"x y"'],
['title=TEXTO', 'title', 'TEXTO'],
["class='a b'", 'class', "'a b'"],
['data-key', 'data-key', undefined]]    
    

Finalmente usamos for...of para extraer las posiciones de ese Array y volcarlo en una lista.

Ejemplo: Expresiones regulares y destructuring

Resultado:
  • Tag:
  • Atributos:
  • Contenido:
Mensaje:
Este ejemplo usa ES6 en modo estricto. Puedes consultar el código JS original de este ejemplo.

Copiando Arrays con destructuring

Cuando asignamos una variable de los tipos String, Number y Boolean a otra variable ambas permanecen independientes. En el siguiente código una segunda variable es asignada a la primera. Luego modificamos la segunda y no afecta a la primera. Con la segunda variable lo que estamos realmente haciendo es una copia del valor de la primera variable.

let a = 1;
let b = a;
b++;
console.log(b); // 2
console.log(a); // 1
    

Para los objetos esto no es así. Si la primera variable es un Array y la asignamos a la segunda, las modificaciones en la segunda afectarán a la primera y al revés. Ahora lo que se produce es una copia de la referencia, de tal forma que ambas apuntan al mismo Array. Cualquier cambio aplicado al Array desde una variable se reflejará en la otra.

let a = [1, 2, 3];
let b  = a;
b.push(4);
console.log(b); // [1, 2, 3, 4]
console.log(a); // [1, 2, 3, 4]
a.push(5);
console.log(b); // [1, 2, 3, 4, 5]
    

Algunas veces necesitamos que esto no suceda y no queda más remedio que copiar el Array, para lo que tendremos que copiar los valores de todas las posiciones a un nuevo Array con estructura equivalente.

Copia superficial de Arrays (Shallow copy)

Con una copia superficial (Shallow copy) los valores literales como String, Number o Boolean se copiarán por valor, mientras que para los objetos se copiará la referencia. Con el original [1, [2, 3]] la copia contendrá lo mismo. En esa copia el 1 se copia por valor mientras que el objeto Array [2, 3] se copia por referencia. Si luego modificamos alguna posición en ese Array en el original también se manifestará dicho cambio en la copia y viceversa.

Con desestructurado y operador resto de elementos de un Array también podemos hacer una copia superficial de una Array. Sería tan simple como let [...copia] = [1, 2, 3]. De hecho es la forma más simple de hacerlo entre todas las siguientes técnicas de copia superficial:

  • Copiando posiciones (items) del Array en uno nuevo.
    let copia = [];
    for (let i=0, maxi=original.length; i<maxi; i++){
        copia.push(original[i]);
    }
                
  • Con métodos de Array y Object como concat(), slice() y assing():
    • let copia = [].concat(original)
    • let copia = original.slice(0);
    • let copia = []; Object.assign(copia, original);
  • Con Destructuring:
    let [...copia] = original;
                

Copia profunda de Arrays (Deep copy)

La única forma de copiar todo por valor es clonando el objeto. Se trata de realizar una copia en profundidad (deep copy) mediante un recursivo que desreferencie los objetos. El siguiente código permite hacerlo:

function clonar(obj){
    let newObj = (obj.constructor.name == "Array") ? [] : {};
    for (let i in obj){
        if (obj.hasOwnProperty(i)){
            if (typeof obj[i] == "object"){
                newObj[i] = clonar(obj[i]);
            } else {
                newObj[i] = obj[i];
            }
        }
    }
    return newObj;
}
            

En el siguiente ejemplo podrá probar todas esas técnicas. Una vez copiado modificaremos el original y comprobaremos si la copia resulta afectada.

Ejemplo: Copiando Arrays

  • original:
  • copia:
  • ¿original === copia?:
  • original modificado:
  • copia:
Este ejemplo usa ES6 en modo estricto. Puedes consultar el código JS original de este ejemplo.

Podrá observar que con referencias no se realiza copia, referenciando ambas variables al mismo Array. Con todos a excepción de clonar se realiza copia superficial, pero las modificaciones en el Array interior se replican en ambos ejemplares. Sólo al clonar el Array obtenemos dos estructuras totalmente independientes.

Valores múltiples retornados desde una función

Una función puede recibir múltiples valores en sus argumentos pero sólo puede retornar un valor. Pero en algunas situaciones necesitamos devolver más de una variable. La forma de hacerlo es agruparlas en un objeto para luego separarlas en el destino de la llamada. Por ejemplo, podríamos tener una función que devuelva dos valores en un Array. En la llamada desestructuramos ese Array para asignarlo a las variables que reciben los dos valores de la función.

function fun(a, b){
    return [a+1, b+1];
}
let [x, y] = fun(1, 2);
console.log(x); // 2
console.log(y); // 3
    

Como ejemplo de una situación real veremos la estructura de funciones en la que se basa el Marco de herramientas WebTools online de este sitio. Se trata de un conjunto de módulos, cada uno destinado a una pieza de herramienta, manejados desde un módulo principal. Cada módulo es un objeto con propiedades y métodos. Cada método es una función dotada de control de errores. El objetivo es llevar a cabo una ejecución controlada a la vez que podemos realizar un seguimiento de lo que está pasando en cada momento.

El siguiente código es una función con ese control de errores. En todas las funciones recibimos los valores a través de los argumentos. Definimos una variable para recoger el posible error que se produzca en la ejecución y otra variable destinada a contener el resultado que se devolverá. El proceso se lleva a cabo en un bloque try-catch. Antes de la devolución presentamos en algún elemento de la página el texto del error. Luego devolvemos un Array cuya primera posición es el texto con el error, o una cadena vacía si no lo hubo, y otra posición para el resultado. Al llamar a esa función desde otro punto del código podemos aplicar desestructurado de datos al Array que nos devuelve aquella función.

//Función con control de errores y devolución 
//de múltiples valores
let procesar = (a, b, c) => {
    let error = "", resultado = 0;
    try {
        //procesa para obtener un resultado
        resultado = (a + b) * c;
    } catch (e){
        error = e.message;
    }
    if (error) presentarError("procesar()", error);
    return [error, resultado];
};
//...................................
//llamada desde otro punto del código
let error = "", valor = 0;
//...ahora necesitamos llamar a procesar() y es aquí
//donde podemos aplicar el DESTRUCTURING
[error, valor] = procesar(1, 2, 3);
if (!error){
    //hace algo con el valor...
}
    

Como aplicación de lo anterior tenemos el siguiente ejemplo interactivo que usa dos funciones. Una es obtenerColor() que extrae los componentes RGB de tres campos de texto en la página. La otra función es colorearFondo() que aplica el color al fondo de un elemento cuyo identificador también está en un campo de texto. Podemos probar valores no permitidos en los campos, observándose como actúa el control de errores.

Ejemplo: Destructurando valores devueltos por una función

Este ejemplo usa ES6 en modo estricto. Puedes consultar el código JS original de este ejemplo.

Y hasta aquí el destructuring, una nueva característica de ES6 que podemos y debemos usar porque nos simplificará muchos problemas.