3

¿Sábes copiar objetos en javaScript?

Uses el lenguaje que uses siempre vas a necesitar copiar valores. Hoy nos concentraremos en como hacerlo con JavaScript.
Tener este conocimiento del lenguaje te evitará muchos bugs en el futuro, te facilitará comprender y utilizar la inmutabilidad; algo importante si te interesa tener buenas prácticas o utilizas frameworks y librerías.

Qué métodos tenemos para copiar

En Platzi existen varios cursos y blogs que hablan e introducen a este tema como por ejemplo:

Copiar un valor es sencillo, se emplea la asignación(el signo igual =) y se pasa a la variable el valor que queremos copiar. El problema aparece con los objetos.
Los recursos anteriormente mencionados solo brindan las soluciones básicas(aunque muchas de las veces son suficiente) para copiar un objeto. Por ejemplo:

// Estos solo copian objetos sencillos de un solo nivel y sus funciones pero no objetos anidados y tampoco los getters y setters.let copy = Object.assign( { }, objeto )
let copy = { ...objeto }

// Copia objetos anidados pero no los métodos ni los getters y setters.JSON.parse( JSON.stringify( objeto ) )

Sí llegaste hasta aquí es porque quieres hacer una deep copy( copia profunda ) a un objeto y copiar “todo” el objeto.
Por tanto, nuestro método debe ser capaz de copiar un objeto que contenga otros objetos anidados, métodos y getters o setters. O sea, un objeto tan complejo como el siguiente:

const Person = {
	_name: 'Christian',
	lastName: 'Boffill',
	age: 29,
	things: [ 't-shirt', 'tv', ['shoe', 'smartphone'] ],
	friends: {
		name: 'Rose',
		age: 30,
		book: {
			title: 'Game of JS',
			pages: 10,
		},
	},
	greeting() {
		return'Hello, how are you?'
	},
	get name() {
		returnthis._name
	},
	set name(newValue) {
		this._name = newValue
	},
}

El método FullCopy() ¡copia sin preocuparte!

Funcionamiento: Recibe un valor y retorna una copia del mismo.

functionFullCopy(value) {
	if (TypeCheck(value)[0] === 'array') returnJSON.parse(JSON.stringify(value))
	if (TypeCheck(value)[0] !== 'array' && TypeCheck(value)[0] !== 'object') return value
	elsereturn CopyObj(value)
}

Cuando el valor entra a la función analizamos cuál es su tipo(number, string, boolean, array, object, date, null o undefined) y en función del tipo hacemos la copia. Este método, como pueden observar, trabaja en conjunto con otros dos: TypeCheck() y CopyObj() , los cuales veremos a continuación.

El método TypeCheck() ¡identifica el tipo de valor!

Funcionamiento: Recibe un valor cualquiera y regresa un arreglo de dos posiciones. En la posición cero tiene el tipo y en la posición uno tiene el nombre del prototipo del que es instancia dicho objeto. Ejemplo: ['number', 'Number']

const TypeCheck = value => {
	let type = Object.prototype.toString.call(value).match(/\s([\w]+)\]$/)[1].toLowerCase()
	let prototype =
		type === 'null' || type === 'undefined'
			? ''
			: Object.getPrototypeOf(value).constructor.name
	return [ type, prototype ]
}

El método recursivo CopyObj()

Funcionamiento: Recibe un objeto y regresa una copia.
Si el tipo es un arreglo hacemos la copia con JSON.parse y JSON.stringify(). Si el tipo es un objeto hacemos uso de la recursividad llamando a CopyObject() y pasándole el nuevo objeto.

Cuando no se encuentre ni con objetos ni con arreglos, necesitamos copiar los descriptores de la key y pasarlos a la nueva key en el objeto copia.

const CopyObj = obj => {
	let copy = {}
	for (let key in obj) {
		if (TypeCheck(obj[key])[0] === 'object')
            copy[key] = CopyObj(obj[key])
		else {
			if (TypeCheck(obj[key])[0] === 'array')
              copy[key] = JSON.parse(JSON.stringify(obj[key]))
			elseObject.defineProperty(copy, key, Object.getOwnPropertyDescriptor(obj, key))
		}
	}
	return copy
}

Gracias a Object.defineProperty() y a Object.getOwnPropertyDescriptor() podemos definir las propiedades o keys del objeto copy usando los descriptores originales del objeto que se está clonando. Esto hace que se copien los métodos, los setters y getters de manera correcta.

Descriptores, por si tenías dudas 😃

Un descriptor es un objeto como este: { value: 'algo', writable: true, enumerable: true, configurable: true } . A cada key de un objeto le corresponde un descriptor parecido.

Si utilizas Object.getOwnPropertyDescriptors(obj) puedes ver cada descriptor de cada key del objeto. También puedes emplear Object.getOwnPropertyDescriptor(obj, 'la_key') para ver el descriptor de una key.

Pero si quieres personalizar los valores, hazlo con: Object.defineProperty( obj, 'la_key', { value: 'algo', writable: true, enumerable: true, configurable: true} ) que le agrega al objeto dado la key con el descriptor que le pasamos.

Nota: En los cursos de POO que mencioné más arriba puedes aprender a como manejar a writable, enumerable y configurable para mejorar el encapsulamiento de tus objetos. Solo te adelanto que son true por defecto.

Hagamos una copia

Toma las tres funciones y el objeto Person y cópialo en un mismo archivo llamado deepCopy.js.
En el caso que no quieras hacer lo anterior, pero si te interesa usar estas funciones de manera más sencilla en tus proyectos, preparé esta librería para ti: FullCopy en NPM y FullCopy en GitHub. Es muy fácil de instalar: npm i full-copy. Únicamente tienes que importar las mismas funciones y usarlas.

// En FullCopy tendremos la copia que resulta de llamar a DeepCopy() pasandole el objeto persona que vimos más arriba:let newPerson = FullCopy(Person)
// Al ejecutarse la línea anterior ya está lista la copia, solo hay que comporbar que funciona// Para hacer esto modificamos algunas keys del objeto newPerson y luego comprobamos que el cambio solo se halla realizado en newPerson y no en los dos:
newPerson.name = 'Robert'
newPerson.things.push('xbox')
newPerson.friends.book.title = 'Type Wow Script'

Para ver los resultados podemos usar console.log() o console.table() en el navegador o desde la terminal con la extensión Code Runner de Visual Studio Code.

console.log(Person)
console.log(newPerson)
console.log(newPerson.greeting(), newPerson.name)
// Person
{
  _name: 'Christian',
  lastName: 'Boffill',
  age: 29,
  things: [ 't-shirt', 'tv', [ 'shoe', 'smartphone' ] ],
  friends: { name: 'Rose', age: 30, book: { title: 'Game of JS', pages: 10 } },
  greeting: [Function: greeting],
  name: [Getter/Setter]
}

// newPerson
{
  _name: 'Robert',
  lastName: 'Boffill',
  age: 29,
  things: [ 't-shirt', 'tv', [ 'shoe', 'smartphone' ], 'xbox' ],
  friends: {
    name: 'Rose',
    age: 30,
    book: { title: 'Type Wow Script', pages: 10 }
  },
  greeting: [Function: greeting],
  name: [Getter/Setter]
}

Hello, how are you? Robert

Como ven modificamos a newPerson y no se alteró el objeto original. Igual pueden comprobar que si se modifica el original no se modifica la copia.

Conclusiones

Aunque en JavaScript no exista un método nativo definitivo para copiar el valor que sea, podemos, mientras lo crean, experimentar con el lenguaje y conocer su esencia. Siéntete libre de implementar el algoritmo en tus proyectos y de contribuir para mejorarlo.

Espero te sirvan las funciones o la librería. Comenta que te pareció y si quieres saber más al respecto.

Escribe tu comentario
+ 2