Cómo copiar objetos en JavaScript

Una tarea muy común en programación, independientemente del lenguaje, es copiar (o clonar) un objeto por valor, en lugar de copiar por referencia. La diferencia es que...

Introducción

Una tarea muy común en programación, independientemente del lenguaje, es copiar (o clonar) un objeto por valor, en lugar de copiar por referencia. La diferencia es que al copiar por valor, tiene dos objetos no relacionados con el mismo valor o datos. Copiar por referencia significa que tiene dos objetos que apuntan a los mismos datos en la memoria. Esto significa que si manipula el objeto A, por ejemplo, también manipulará el objeto B, ya que ambos hacen referencia a los mismos datos subyacentes.

En este artículo, repasaré algunas de las formas en que puede copiar objetos por valor en JavaScript. Le mostraré cómo puede hacer esto utilizando bibliotecas de terceros y escribiendo su propia función de copia.

Nota: dado que Node.js es solo un tiempo de ejecución creado en el motor de JavaScript V8, todos los métodos de clonación que muestro en este artículo también funcionarán para Node.

Bibliotecas de terceros

Hay una serie de bibliotecas de terceros populares que tienen esta funcionalidad integrada, que veremos en las próximas secciones. En mi opinión, estas son la mejor solución para la mayoría de los casos de uso simples, ya que han sido probadas exhaustivamente y actualizadas continuamente. Escribir este tipo de código usted mismo no es fácil, por lo que es muy útil poder usar un código que tenga muchos ojos en él.

Lodash

La biblioteca Lodash proporciona algunos métodos diferentes para copiar o clonar objetos, según su caso de uso.

El método más genérico es el método clone(), que proporciona copias superficiales de los objetos. Funciona simplemente pasando el objeto como primer argumento, y se devolverá la copia:

1
2
3
4
5
const _ = require('lodash');

let arrays = {first: [1, 2, 3], second: [4, 5, 6]};
let copy = _.clone(arrays);
console.log(copy);
1
{ first: [ 1, 2, 3 ], second: [ 4, 5, 6 ] }

Esto significa que el objeto "nivel superior" (o matriz, búfer, mapa, etc.) se clona, ​​pero los objetos más profundos se copiarán por referencia. El siguiente código demuestra que la matriz primera en el objeto matrices original es el mismo objeto que la matriz primera en el objeto copia:

1
2
3
4
5
const _ = require('lodash');

let arrays = {first: [1, 2, 3], second: [4, 5, 6]};
let copy = _.clone(arrays);
console.log(copy.first === arrays.first);
1
true

Si prefieres que se copien todos los objetos, tanto superficiales como profundos, entonces querrás usar el método cloneDeep() en su lugar:

1
2
3
4
5
const _ = require('lodash');

let arrays = {first: [1, 2, 3], second: [4, 5, 6]};
let copy = _.cloneDeep(arrays);
console.log(copy);
1
{ first: [ 1, 2, 3 ], second: [ 4, 5, 6 ] }

Este método funciona mediante la clonación recursiva de todos los valores en cualquier nivel de profundidad.

Al ejecutar la misma verificación de igualdad anterior, podemos ver que las matrices original y copiada ya no son iguales, ya que son copias únicas:

1
2
3
4
5
const _ = require('lodash');

let arrays = {first: [1, 2, 3], second: [4, 5, 6]};
let copy = _.cloneDeep(arrays);
console.log(copy.first === arrays.first);
1
false

Lodash ofrece algunos métodos de clonación más, incluidos cloneWith() y cloneDeepWith(). Ambos métodos aceptan otro parámetro llamado personalizador, que es una función utilizada para ayudar a producir el valor copiado.

Entonces, si desea usar alguna lógica de copia personalizada, puede pasar una función para manejarla dentro del método de Lodash. Por ejemplo, supongamos que tiene un objeto que contiene algunos objetos Date, pero desea que se conviertan en marcas de tiempo al ser copiados, podría hacerlo así:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const _ = require('lodash');

let tweet = {
    username: '@ScottWRobinson',
    text: 'I didn\'t actually tweet this',
    created_at: new Date('December 21, 2018'),
    updated_at: new Date('January 01, 2019'),
    deleted_at: new Date('February 28, 2019'),
};
let tweetCopy = l.cloneDeepWith(tweet, (val) => {
    if (l.isDate(val)) {
        return val.getTime();
    }
});
console.log(tweetCopy);
1
2
3
4
5
{ username: '@ScottWRobinson',
  text: 'I didn\'t actually tweet this',
  created_at: 1545372000000,
  updated_at: 1546322400000,
  deleted_at: 1551333600000 }

Como puede ver, los únicos datos que se modificaron con nuestro método fueron los objetos Date, que ahora se han convertido en marcas de tiempo de Unix.

Guion bajo

El método Guion bajo clone() funciona de la misma manera que el método clone() de Lodash. Solo proporciona una copia superficial del objeto dado, y los objetos anidados se copian por referencia.

El mismo ejemplo anterior demuestra esto:

1
2
3
4
5
const _ = require('underscore');

let arrays = {first: [1, 2, 3], second: [4, 5, 6]};
let copy = _.clone(arrays);
console.log(copy.first === arrays.first);
1
true

Desafortunadamente, la biblioteca de subrayado no parece tener ningún método para manejar la copia profunda. Puede implementar esta lógica por su cuenta (usando parte de la lógica que se muestra a continuación) y seguir usando el método clonar de Underscore para la copia superficial, o puede probar una de las otras soluciones de este artículo.

Soluciones personalizadas

Como mencioné anteriormente, asumir este desafío por sí mismo es difícil ya que hay muchos casos (y casos extremos complicados) para manejar cuando se clona un objeto en JavaScript. Aunque, si se hace correctamente, podrá agregar una buena personalización dentro de su método que de otra manera no sería posible.

Uso de métodos JSON {#uso de métodos json}

Una solución citada a menudo es simplemente usar los métodos JSON.stringify y JSON.parse para su ventaja, así:

1
2
3
let arrays = {first: [1, 2, 3], second: [4, 5, 6]};
let copy = JSON.parse(JSON.stringify(arrays));
console.log(copy);
1
{ first: [ 1, 2, 3 ], second: [ 4, 5, 6 ] }

Esto lo dejará con un objeto profundamente copiado y funciona muy bien para objetos simples que se convierten fácilmente a JSON.

Podemos verificar esto nuevamente usando el mismo control que el anterior:

1
console.log(copy.first === arrays.first);
1
false

Si sabe que su objeto es fácilmente serializable, esta podría ser una buena solución para usted.

Escribir uno propio desde cero

Si por alguna razón ninguna de las otras soluciones funciona para usted, tendrá que escribir su propio método de clonación.

Como no confío en mí mismo para implementar correctamente un método de clonación completo (y me arriesgo a que los lectores copien mis errores en su código de producción), copié la siguiente función de clonación de [esta esencia](https://gist. githubusercontent.com/gdibble/e429544ab8fa931055b2f02a1ec5739d/raw/4186200360c1820b26ecb234094cc22b1e3d567b/clone.js), que copia objetos de forma recursiva y parece funcionar en muchos de los tipos de datos comunes con los que se encontrará en JavaScript.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
function clone(thing, opts) {
    var newObject = {};
    if (thing instanceof Array) {
        return thing.map(function (i) { return clone(i, opts); });
    } else if (thing instanceof Date) {
        return new Date(thing);
    } else if (thing instanceof RegExp) {
        return new RegExp(thing);
    } else if (thing instanceof Function) {
        return opts && opts.newFns ? new Function('return ' + thing.toString())() : thing;
    } else if (thing instanceof Object) {
        Object.keys(thing).forEach(function (key) { newObject[key] = clone(thing[key], opts); });
        return newObject;
    } else if ([ undefined, null ].indexOf(thing) > -1) {
        return thing;
    } else {
        if (thing.constructor.name === 'Symbol') {
            return Symbol(thing.toString().replace(/^Symbol\(/, '').slice(0, -1));
        }
        return thing.__proto__.constructor(thing);
    }
}

Esta función funciona manejando casos específicos cuando es necesario (como arreglos, expresiones regulares, funciones, etc.), y luego para todos los demás tipos de datos (como números, cadenas, booleanos, etc.) por defecto a thing's propio constructor para copiar el valor. Si la ‘cosa’ es un objeto en sí mismo, entonces simplemente se llama recursivamente a sí mismo en los atributos secundarios de la ‘cosa’.

Consulte la esencia completa en el enlace anterior para conocer todos los tipos de datos y casos extremos en los que se ha probado.

Conclusión

Si bien es simple en teoría, en la práctica, copiar un objeto en JavaScript es cualquier cosa menos simple. Por suerte, existen bastantes soluciones disponibles para su uso, como cloneDeep en Lodash, o incluso los métodos integrados JSON. Y si por alguna razón ninguno de ellos es adecuado, es posible escribir su propio método de clonación, siempre que lo pruebe a fondo.

Buena suerte, y nosotros sabemos si tiene alguna idea, idea o consejo en los comentarios.