/

Operador Spread - Javascript #6

“La simplicidad es la gloria de la expresión” Walt Whitman

Operador Spread (…js)

Un operador que ayuda mucho a tener una programación inmutable y que aporta mucha legibilidad a nuestro código es el operador spread o coloquialmente también conocido como “punto punto punto”.

La traducción de spread es propagador, y es que el operador ... nos permite “propagar” las propiedades de los elementos a los que se les aplica. Propagar es una manera un poco abstracta de verlo. El objetivo es que las propiedades del elemento sobre el que se aplica el operador se expandan donde son esperadas, por ejemplo en los argumentos de una función.

const array = [ 1, 2, 3 ];
const result = [ ...array, 4, 5, 6 ];

// result: [ 1, 2, 3, 4, 5, 6 ]

En el ejemplo, el array [ 1, 2, 3 ] se propaga en un nuevo array al ejecutar la segunda línea y dar valor a la variable result con sus valores y los que se definen a continuación.

Operador spread en objetos

Los objetos también pueden ser expandidos con .... Veamos el ejemplo:

const obj = {
    name: 'pepe',
    age: 27
};

const result = {
    ...obj,
    country: 'spain'
}

// result: {
//      name: pepe,
//      age: 27,
//      country: 'spain'   
// }

El nuevo objeto result tiene las dos propiedades que tiene el objeto obj. El objeto obj se ha expandido dentro de result. Esto también se puede hacer sobreescribiendo alguna propiedad de esta manera:

const obj = {
    name: 'pepe',
    age: 27,
    country: 'spain'
};

const result = {
    ...obj,
    country: 'france'
}

// result: {
//      name: pepe,
//      age: 27,
//      country: 'france' 
// }

Al sobreescribir la propiedad country el uso del operador spread queda reducido a la expansión de todas las propiedades que no están sobreescritas. Así, podemos hacer que un objeto mute solo cambiando una propiedad de un modo sencillo.

Operador spread en arrays

En la definición del operador ... hemos visto una manera de poder concatenar arrays. Ahora vamos a ver en profundidad las operaciones que podemos hacer con los mismos.

Push en un array

La función push se hace más sencilla con este operador:

const array = [ 1, 2, 3 ];
const arrayPush = [ 4, 5 ];

const result1 = Array.prototype.push.apply(array, arrayPush);
const result2 = array.push(...arrayPush);
// result1 & result2 = [ 1, 2, 3, 4, 5 ]

Al expandir los elementos de arrayPush en la variable array podemos usar la función push directamente sin tener que recurrir al push de la clase Array y todo queda mucho más legible.

Este es un ejemplo para que veamos la equivalencia del push de la clase array con el uso de ...; pero lo que se suele emplear para el push es directamente la concatenación de arrays que veremos a continuación, ya que es equivalente a realizar cualquier añadido al array, aunque sea solo un elemento.

Concatenación de arrays

Para concatenar dos arrays se usa la función definida en el prototype de Array concat. Podemos obtener el mismo resultado con concat que usando spread:

const array = [ 4, 5, 6 ];
const result = [ 1, 2, 3 ].concat(array);

const resultWithSpread = [ 1, 2, 3, ...array]

// result & resultWithSpread = [ 1, 2, 3, 4, 5, 6 ]

En este caso, el resultado de concatenar los arrays es el mismo; pero en mi opinión el operador ... le da mucha más expresividad a nuestro código ya que lo hace mucho más fácil de leer.

Operación splice en un array

La función splice combina dos arrays. Es una función algo compleja que recibe como parámetros el inicio de donde se inserta el nuevo array, el número de elementos que sustituye y el nuevo array:

const numbers = [ 4, 5 ];
numbers.splice(0, 0, [1, 2, 3]);
// numbers: [ 1, 2, 3, 4, 5]

const numbers = [ 1, 2, 2, 4, 5 ];
numbers.splice(1, 2, [3, 3]);
// numbers: [ 1, 3, 3, 4, 5]

const numbers = [ 1, 2, 3, 5 ];
numbers.splice(3, 0, [4]);
// numbers: [ 1, 2, 3, 4, 5]

Estas operaciones usando el operador spread pueden quedar mucho más legibles. Además otra ventaja de usar spread es que se crea un nuevo array y no se modifica el que existe como hace la función splice, lo que viene mejor para una programación inmutable. Para ver donde colocar el nuevo array nos ayudaremos de la función slice, que nos da el subconjunto del array que necesitamos:

const array = [ 1, 2, 3 ];
const numbers = [ 4, 5 ];
const result = [
    ...array,
    ...numbers
];
// result: [ 1, 2, 3, 4, 5]
numbers.splice(0, 0, array);
// numbers: [ 1, 2, 3, 4, 5]

const array = [ 3, 3 ];
const numbers = [ 1, 2, 2, 4, 5 ];
const result = [
    ...numbers.slice(0, 1),
    ...array,
    ...numbers.slice(3)
];
// result: [ 1, 3, 3, 4, 5]
numbers.splice(1, 2, array);
// numbers: [ 1, 3, 3, 4, 5]

const array = [ 4 ];
const numbers = [ 1, 2, 3, 5 ];
const result = [
    ...numbers.slice(0, 3),
    ...array,
    ...numbers.slice(3)
];
// result: [ 1, 2, 3, 4, 5]
numbers.splice(3, 0, array);
// numbers: [ 1, 2, 3, 4, 5]

Si lo pensamos con detenimiento una vez vista la función slice esto es equivalente:

const array = [ 1, 2, 3 ];

const result1 = [ array.slice() ];
const result2 = [ ...array ];

// result1 & result2 = [ 1, 2, 3 ]

Operador spread y los parámetros Rest

El resultado del operador spread también puede se puede poner al pasarle los parámetros a una función:

const numbers = [ 1, 2, 3 ];

function sum(a, b, c) {
    return a + b + c;
}

const result = sum(...numbers);
// result: 6

Esto ya lo habíamos hecho en la función push del array. Podemos combinarlo como queramos, usarlo es muy cómodo:

const dateData = [1989, 1, 7]; 
const d = new Date(...dateData);

En cambio, si queremos poner la expresión ... en los propios parámetros de una función, el comportamiento es el contrario. En lugar de convertir una array en una lista de elementos, convierte una lista de elementos en un array. Podemos ver este comportamiento usando la función suma del revés:

// En su expresión con arrays
function sum(...numbers) {
    let total = 0;
    for (var i=0; i < numbers.length; i++) {
        total += numbers[i];
    }
    return total;
}

// Aunque mejor podemos poner un reduce
const sum = (...numbers) =>
    numbers.reduce(
        (total, nextNum) => total + nextNum, 
        0
    );

const result = sum(1, 2, 3);
// result: 6

Para acordarse de como poder usar reduce podéis visitar trabajando con arrays.

La expresión ... utilizada como parámetros de una función se denomina parámetros Rest y, como hemos dicho anteriormente es justo la expresión contraria al operador spread.

Destructuring y los parámetros rest

Se puede combinar los parámetros rest junto con el destructuring.

const numbers = [ 1, 2, 3 ];

const { firstNumber, ...otherNumbers } = numbers;

// firstNumber: 1
// otherNumbers: [ 2, 3 ]

Podemos combinarlo de la manera que queramos, usando ... al principio, al final o en medio.

const numbers = [ 1, 2, 3, 4, 5 ];

const { firstNumber, ...otherNumbers, lastNumber } = numbers;

// firstNumber:  1
// otherNumbers: [ 2, 3, 4 ]
// lastNumber:   5

Y podríamos usar todo esto en los parámetros de una función.


const greeting = ({ country, name, ...person }) => country === 'spain' ? `Hola ${name}!` : `Hello ${name}!`;

Objetos inmutables

El operador spread es muy útil a la hora de trabajar con objetos inmutables, ya que si queremos crear un nuevo array o un nuevo objeto pero cambiando solo una de sus propiedades podemos usarlo con lo visto anteriormente.

Si por ejemplo tenemos una lista de objetos de tipo persona y queremos cambiar el nombre de una de ellas y hacer un nuevo objetos para pasarlo al estado de nuestro componente en React por poner un caso de uso, podríamos aplicar algo parecido a:


const people = [
    {
        id: 1,
        name: 'pepe',
        age: 45,
        country: 'spain',
    },
    {
        id: 2,
        name: 'jaime',
        age: 34,
        country: 'france',
    },
    {
        id: 3,
        name: 'laura',
        age: 37,
        country: 'spain',
    },
    {
        id: 4,
        name: 'gema',
        age: 20,
        country: 'usa',
    },
    {
        id: 5,
        name: 'donnald',
        age: 18,
        country: 'usa',
    },
];

const changePersonName(personid, newname) {
    const newArray = people.map(person => 
        person.id === personid 
        ? ({
            {
                ...person,
                name: newname,
            }
        })
        : person);
    return newArray;
};

Deep copy

El ejemplo anterior es sencillo y tiene un pequeño efecto demo. Hay que tener cuidado cuando lo que queremos hacer es un deep copy, es decir, el copiado no solo de propiedades primitivas sino de propiedades que sean objetos. En el caso de que la propiedad que copiemos sea un objeto se estará copiando solamente la referencia:

let person =  {
    id: 5,
    name: 'donnald',
    age: 18,
    country: {
        name: 'france',
        capital: 'paris',
    },
};
const newPerson = {
    ...person,
    age: 28,
}
person.country.name = 'spain';

console.log(newPerson.country);
// country: {
//    name: 'spain',
//    capital: 'paris',
// }

Por ello si queremos hacer un copiado en profundidad (deep copy) debemos seguir alguna otra estrategia, por ejemplo serializar un nuevo objeto:

let person =  {
    id: 5,
    name: 'donnald',
    age: 18,
    country: {
        name: 'france',
        capital: 'paris',
    },
};
const newPerson = JSON.parse(JSON.stringify(person));
person.country.name = 'spain';

console.log(newPerson.country);
// country: {
//    name: 'france',
//    capital: 'paris',
// }

Object assign

El operador spread y la función Object.assign({}, person) hacen exactamente lo mismo siempre que el primer argumento de Object.assign sea un objeto vacío. De hecho, spread usa por debajo Object.assign si el navegador lo soporta.

const person =  {
    id: 5,
    name: 'donnald',
    age: 18,
    country: 'france',
};
const newPerson = {
    ...person,
}

const newPersonAssign = Object.assign({}, person);

// newPerson y newPersonAssign serian una copia exacta de person

El objetivo de Object.assign es copiar las propiedades de un objeto origen a un objeto destino. El primer parámetro de la función es el objeto destino. Por ello, para usar inmutabilidad y ser idéntico al comportamiento de spread, el primer parámetro debería ser ese objeto vacío. Veamos un ejemplo sin este objeto vacío:

let newPerson = {
    id: 6,
};

const person =  {
    id: 5,
    name: 'donnald',
};

const newPersonAssign = Object.assign(newPerson, person);

// newPersonAssign: {
//  id: 6,
//  name: 'donnald',
// }

// newPerson: {
//  id: 6,
//  name: 'donnald',
// }

Object.assign tiene los mismos problemas que spread con el copiado en profundidad.

Por tanto, si lo que buscamos es una solución para la inmutabilidad, lo mejor por legibilidad en el código es usar el operador spread en lugar de Object.assign. Es verdad que en perfonmance Object.assign tiene un pequeño mejor resultado, pero es casi despreciable.

Para ver más detalles de Object.assign podéis mirar aquí.

Conclusiones

Debemos tener siempre presente un operador como spread para poder conjugarlo en cualquier ocasión y dejar nuestro código mucho más legible.

Usar ... da energía a nuestras líneas de Javascript.

Happy coding!