Valores primitivos y objetos
Cuando programamos, es usual invocar funciones y pasar argumentos, sin embargo es posible que no conozcamos el manejo que le da JavaScript a esos parámetros, motivo por el cual a veces obtenemos resultados inesperados.
💡 Primero que todo vamos a aclarar los términos argumetos y parámetros:
- argumetos hace referencia a los valores que le enviamos a una función.
- parámetros hace referencia a los valores que recibe la función y son especificados en la firma de la función.
// a y b son los parámetros de suma function suma (a, b) { return a + b; } var x = 5; // x y 8 son los argumentos enviados a suma var r = suma(x, 8);
⭐ Al enviar argumentos con valor primitivo (string, number, boolean, null, undefined)
, el parámetro de la función que recibe el valor crea una copia by value.
Esto significa que cualquier cambio que hagamos sobre ese parámetro, sólo será válido dentro de la función y no afectará al argumento original. Veamos el siguiente ejemplo:
function test(msg) { msg = "modificado"; console.log(msg); return msg; } var msg1 = "JavaScript rocks!", msg2 = test(msg1); console.log(msg1); //"JavaScript rocks!"
😮 Podemos ver que después de ejecutar la función test()
, el argumento msg1
sigue intacto, aunque dentro de la función fue modificado. Esto mismo ocurre cuando asignamos un valor primitivo a una variable; se crea una copia de ese valor.
var msg1 = "JS is Awesome!", msg2 = msg1; msg1 = "JS sucks!"; console.log("msg1 = ", msg1); // "JS sucks!" console.log("msg2 = ", msg2); // "JS is Awesome!"
➡ Sin embargo cuando trabajamos con objetos (Object, Array, RegExp, Date...)
, ya sea que lo pasemos como argumento de una función o que lo asignemos a otra variable, en realidad estamos pasando su referencia en memoria (by reference) y no una copia de su valor. 😳 Esto significa que cualquier cambio que hagamos sobre la “copia” del objeto, se verá reflejado en el objeto original. Veamos un ejemplo:
function setCenter(x) { x.top = "50%"; x.left = "50%"; } var point1 = { top: 1, left: 1 }; // pensamos crear una copia, // pero en realidad pasamos la referencia var point2 = point1; setCenter(point2); console.log("point1: ", point1); console.log("point2: ", point2);
😕 Observamos que después de invocar la función setCenter()
, ambos objetos fueron modificados! Esto ocurrió porque point2
no almacena un valor primitivo, sino un Object
, de tal manera que al asignarlo a otra variable, o enviarlo como parámetro a una función, en realidad estamos pasando la referencia en memoria del objeto original, y NO creando una copia.
Veamos un ejemplo más:
function View (name) { "use strict"; this.name = name || "Home"; this.visits = 0; } View.calls = 0; function visit(x) { x.visits = 1 + (x.visits || 0); x.referrer = document.referrer; x.constructor.calls++; } var aboutView = new View("About"); console.log(aboutView); visit(aboutView); console.log(aboutView);
Como esperábamos, después de invocar la función visit()
se modificó el objeto original aboutView
.
¿Y qué dice el standard?
A primitive value is a member of one of the following built-in types: Undefined, Null, Boolean, Number, and String; an object is member of the remaining built-in type: Object; and a method is a function associated with an object via a property. — ECMAScript
“Un valor primitivo es miembro de uno de los siguientes tipos integrados: Undefined, Null, Boolean, Number y String; un objeto es miembro del tipo integrado restante: Object; y un método es una función asociada a un objeto mediante una propiedad”
De lo anterior podemos asumir que una función también está asociada con un objeto (by reference) — Lectura recomendada: Data Types In JavaScript
¿Porqué clonar objetos?
⭐ Si queremos crear una verdadera copia, o evitar que una función modifique el objeto original, entonces debemos clonar el objeto.
Clonando un Array
Para clonar el contenido de un array, todo lo que se necesita es utilizar el método slice(), enviando el valor 0
como primer argumento, para copiar todo el array:
var list = ["A",1,2,3,5,8,13]; //clonamos el array original (shallow copy) var cloned = list.slice(0); //modificamos el primer elemento del array original list[0] = 9; //agregamos un elemento al inicio del array original list.unshift("X"); //agregamos un elemento al final del array clonado cloned.push(21); console.log("original: ", list); console.log("cloned: ", cloned);
En el código anterior clonamos el array original copiando los valores primitivos, por tal motivo podemos alterar los dos arrays independientemente. Sin embargo, si el array contiene elementos de tipo Object
, entonces aquellos elementos del array que no sean primitivos, almacenarán una nueva referencia al objeto original, es decir, se creará una copia de su referencia y no de su valor 😮
var list = [{id: 1, age: 30}, {id: 2, age: 42}]; //clonamos el array original (shallow copy) var cloned = list.slice(0); console.log("list[0]: ", list[0]); console.log("cloned[0]: ", cloned[0]); //modificamos el primer elemento del array clonado cloned[0].age = 21; //ambos fueron modificados console.log("original: ", list); console.log("cloned: ", cloned);
Para crear una copia absoluta de todos los elementos del array, debemos iterar recursivamente todos los elementos del array y clonar cada elemento (deep copy).
➡ Ver la implementación Objetos con estructura circular y Deep copying of Objects and Arrays.
Shallow copy vs Deep copy
- Shallow copy es una copia superficial de los elementos de un array o las propiedades de un objeto, en donde la copia solo tiene un nivel de profundidad. Si un objeto tiene propiedades que son objetos, o un array contiene elementos que son objetos, entonces éstos son copiados por referencia, lo que significa que modificar alguna de sus propiedades resultará en afectar también el objeto original.
- Deep copy es una copia a profundidad de los elementos de un array o las propiedades de un objeto, en donde la copia se hace recursivamente iterando sobre cada elemento o propiedad a clonar. De esta manera se asegura que cada elemento o propiedad clonada, tenga su propio espacio en memoria. Esto significa que al modificar los valores del objeto clonado, no se afectará el objeto original.
➡ Los siguientes métodos de clonación son efectivos para copiar objetos planos (que sigan el estándar JSON), no objetos que posean estructuras circulares o con apuntadores a funciones, en cuyo caso, es mejor crear un clonador que se ajuste a esos requerimientos específicos. (Vea la sección Objetos con estructura circular)
Deep copy usando JSON.parse
/* @throws TypeError: Converting circular structure to JSON. @desc No copia funciones ni estructuras circulares. Debe respetar estrictamente los valores permitidos por JSON. */ function cloneJSON (obj) { return JSON.parse(JSON.stringify(obj)); }
Deep copy usando jQuery.extend
/* @throws RangeError: Maximum call stack size exceeded. @desc Copia funciones, pero no estructuras circulares. */ //clonar arrays con jQuery function jCloneArray (arr) { return jQuery.extend(true, [], arr); } //clonar objetos con jQuery function jCloneObject (obj) { return jQuery.extend(true, {}, obj); }
(function () { var list = [{x:2, y:3}, {x:5, y:8}], cloned = list.slice(0); //shallow copy //añadimos al final del array original list.push({x:13, y:21}); console.log("Shallow copy (slice)"); console.log("original:", list); //3 elementos console.log("clonado:", cloned); //2 elementos //modificamos el item[1] clonado cloned[1].x = null; console.log("original, ver [1]:", list); console.log("shallow copy slice:", cloned); //clonamos con deep copy var clone1 = cloneJSON(list), clone2 = jCloneArray(list); //modificamos el item[0] del array original list[0].x = "original"; console.log("original, ver [0]:", list); console.log("deep copy JSON:", clone1); console.log("deep copy jQuery:", clone2); })();
Clonando DOM Nodes
Para clonar nodos DOM (objetos HTML) se debe tener en cuenta que algunas de las propiedades del nodo hacen referencia hacia sí mismo. Veamos el siguiente ejemplo para dar mayor claridad:
// creamos un elemento <p> var pnode = document.createElement("p"); text = document.createTextNode("I AM THE WEASEL"); pnode.id = "mytest"; pnode.appendChild(text); document.body.appendChild(pnode); var node1 = document.getElementById("mytest"), node2; // accedemos al nodo padre y buscamos al hijo mytest node2 = node1.ownerDocument.getElementById("mytest"); node2.innerHTML = "texto modificado"; console.log([node1, node2]);
😮 En el ejemplo anterior tenemos una estructura con referencia circular al acceder la propiedad ownerDocument, ya que ésta apunta al documento raíz de node1
, y ésta a su vez, posee una referencia de nuevo hacia el nodo mytest
; es decir que las variables node1
y node2
apuntan al mismo objeto.
Clonar nodos DOM que poseen estructuras circulares no es posible con los métodos vistos anteriormente (JSON.parse y jQuery.extend). 💡 Para lograr esta tarea podemos utilizar el método cloneNode() o el método clone() de jQuery.
var node1 = document.getElementById("mytest"), //true: deep copy, false: shallow copy node2 = node1.cloneNode(true); node1 = jQuery("#mytest"); node2 = node1.clone(); //default: deep copy
⭐ Nota: Los elementos clonados crean un duplicado del nodo, incluyendo todos los atributos y sus valores (el atributo id debe ser modificado antes de insertar el nodo en el documento). El elemento clonado no tiene padre (.parentNode = null)
hasta que sea insertado en el documento, bien sea usando appendChild(), append() o alguno de los mecanismos para la inserción de nodos.
✔ Add elements using jQuery
✔ DOM Insertion, Outside
✔ DOM Insertion, Inside
Objetos con estructura circular
Clonar objetos que poseen estructuras circulares es una tarea que requiere un poco más de esfuerzo; en la sección anterior se hizo énfasis en la clonación de nodos DOM, pero ¿que pasa si el objeto que queremos clonar no es un nodo DOM, sino un objeto plano con propiedades que se referencian entre sí?… veamos el siguiente caso:
var freeman = { name: "Gordon Freeman", character: "Freeman", friends: ["Barney Calhoun"] }; var david = { name: "David Rivera", character: "jherax", friends: ["John Carmack"] }; david.friends.push(freeman); freeman.friends.push(david);
Acá tenemos dos objetos con la propiedad friends
en donde el objeto freeman.friends
tiene una referencia hacia david
, y a su vez david.friends
tiene una referencia hacia freeman
, por lo cual hay referencias cruzadas o estructura circular entre los dos objetos.
Creando el clonador
- Será una función recursiva que retorne un deep copy del objeto que recibe como primer argumento.
- Si es un objeto primitivo, retornamos su valor.
- Si el objeto es una instancia de:
Date, RegExp, Function, String, Number, Boolean,
entonces creamos una nueva instancia a partir de su constructor. - Iteramos recursivamente cada una de las propiedades navegables del objeto, y comenzamos de nuevo en el paso 2.
// Extiende las propiedades del objeto @source en el objeto @dest // Retorna un deep copy de @source function clonex (source, dest) { var prop; // determina si @source es un valor primitivo o una función if (source === null || typeof source !== "object") return source; // determina si @source es un DOM Node if (source.nodeName) return source.cloneNode(true); // determina si @source es una instancia de alguno de los siguientes constructores if (source.constructor === Date || source.constructor === RegExp || source.constructor === Function || source.constructor === String || source.constructor === Number || source.constructor === Boolean) { return new source.constructor(source); } // si el constructor del objeto no es ninguno de los anteriores if (source.constructor !== Object && source.constructor !== Array) return source; // crea un nuevo objeto e itera recursivamente sus propiedades dest = dest || new source.constructor(); for (prop in source) { dest[prop] = clonex(source[prop], dest[prop]); } return dest; }
😛 Hasta ahora nuestro clonador es muy similar a otras soluciones en la web, pero vamos a darle un valor agregado, y es que sea capaz de clonar objetos con estructura circular, ya que si intentamos clonar los objetos freeman
o david
, obtendremos RangeError: Maximum call stack size exceeded
.
//RangeError: Maximum call stack size exceeded var cloned = clonex(freeman);
💡 Lo que este mensaje dice, es que hay una iteración infinita y se alcanzó el máximo de llamadas en el stack para la función recursiva cuando intentaba clonar los objetos de la propiedad friends.
Clonando objetos con estructura circular
💡 Una forma de solucionar este problema, es mantener un caché con los objetos clonados, de manera que si ya se creó, entonces pasamos la referencia de ese objeto y así evitamos que se intente crear infinitamente. Utilizaremos un closure que nos proporcionará un estado interno privado para el clonador.
/** * Clona un objeto (deep-copy) con soporte para referencias circulares * * @param {any} source: el objeto a clonar * @param {Object} dest: (opcional) objeto a extender * @return {any} el nuevo objeto clonado */ var clonex = (function() { var toString = Object.prototype.toString; var CONSTRUCTORS = [Date, RegExp, Function, String, Number, Boolean]; // compara un objeto contra el contexto de ejecución function compare(obj) { return obj === this; } function cloner (source, dest, cache) { var prop; // determina si @source es un valor primitivo o una funcion if (source === null || typeof source !== "object") return source; // revisa si @source ya ha sido guardado en cache if (toString.call(source) === "[object Object]") { if (cache.some(compare, source)) return source; // guarda en cache la referencia de los objetos creados // para prevenir la excepción de estructura circular cache.push(source); } // determina si @source es un DOM Node if (source.nodeName) return source.cloneNode(true); // determina si @source es una instancia de algún constructor if (~CONSTRUCTORS.indexOf(source.constructor)) return new source.constructor(source); if (source.constructor !== Object && source.constructor !== Array) return source; // crea un nuevo objeto e itera recursivamente sus propiedades dest = dest || new source.constructor(); for (prop in source) { dest[prop] = cloner(source[prop], dest[prop], cache); } return dest; } // función retornada en el closure return function clonex (source, dest) { return cloner(source, dest, []); }; }());
🐱 Ahora utilicemos nuestro súper clonador 😀
var freeman, david, cloned; function Freeman() { "use strict"; this.name = "Gordon Freeman"; this.character = "Freeman"; this.friends = ["Barney Calhoun"]; } david = { name: "David", character: "jherax", friends: ["John Carmack"], languages: /javascript|c#|java|vb|sql/i, greeting: function() { return "Hi, I am " + this.name }, info: { birth: new Date() } }; //creamos una instancia freeman = new Freeman(); //creamos la referencia circular freeman.friends.push(david); david.friends.push(freeman); //clonamos el objeto cloned = clonex(david); //modificamos propiedades del objeto clonado cloned.name = "David Rivera"; cloned.friends.push("Jim Rynor"); //vemos que el objeto original NO fue modificado console.log("original:", david.name, david.friends); console.log("clonado:", cloned.name, cloned.friends);
Conclusión
Es importante conocer cómo JavaScript maneja los valores primitivos y los objetos. La intención de éste artículo fue demostrar la importancia que tiene la clonación de objetos para mejorar la calidad de nuestro código, evitando errores comunes o “extraños” comportamientos en nuestra aplicación.
⭐ Recomiendo leer los siguientes artículos:
- Deep copying of Objects and Arrays de James Padolsey
- Deep copy in JavaScript de Oran Looney
😎 Happy coding!
hoy en dia puedes hacerlo asi:
var cosaAClonar = {color:”rojo”,numero:1}
var Clon ={…cosaAClonar};
Por supuesto, si solo quieres hacer un shallow-copy de un objeto
Es decir que unicamente copias las keys en un nuevo objeto, pero los valores que sean Objects, no son clonados, sino que se copia la direccion en memoria del objeto original.
Si deseas hacer un deep-clone del objeto debes usar otras técnicas, de las cuales se mencionan algunas en este mismo post: Shallow-copy vs Deep-copy o puedes usar alguna de las muchas librerias disponibles en npm: deep clone
El articulo fue re-escrito en algunas secciones, y se agregaron nuevos segmentos para aclarar algunos temas.
Francamente, muy didáctico. Es muy fácil cometer errores pensando que estamos haciendo lo correcto pero cuando analizamos en profundidad el problema vemos lo equivocados que estamos.
Muy brillante tu artículo.
Articulos como este deberiamos hacer todos, muchas felicidades, me encanto leer tu articulo, muy bien explicado, y muy buenos ejemplos. Mis admiraciones, sigue así!!
Gracias Martín. Este tipo de artículos explicando utilerías me ha gustado, escribiré otros del mismo estilo.
Reblogged this on JOTA ESE (Y MÁS…) and commented:
Un compañero que hizo un excelente trabajo sobre Clonación de Objetos en Javascript
Compañero, gracias por ayudar con la difusión 😉
Con tu permiso jherax, lo reposteo en mi blog… Excelente trabajo 😉
Mi mas sincera enhorabuena…
Excepcional nivel de JavaScript…
Da gusto leer determinados fragmentos de código.
Todo un recital!
Muchas gracias Nacho 🙂
Me alegra saber que hayas disfrutado el artículo.