Entender JavaScript es saber cómo funcionan los prototipos

Figura
Figura. El prototipo de un objeto se presenta como __proto__. Y es también un objeto.

Entender los prototipos de los objetos es imprescindible para manejarse bien con JavaScript. Por eso hemos agrupado en este tema todos los métodos que tienen que ver con los prototipos.

Podemos llegar a JavaScript desde otros lenguajes de programación que usan Programación orientada a objetos (POO). En ese caso podríamos decirnos lo siguiente: JavaScript, otro lenguaje basado en objetos, si conozco POO tendré bastante ventaja. Pero es una gran equivocación pensar así. JavaScript está basado en objetos, pero la relación entre ellos se produce a través de los prototipos. Y esto nada tiene que ver con POO, aunque la relación prototípica pueda simular bastante bien la POO.

Object.create(proto, props)

ES5

El método Object.create(proto) nos permite crear un objeto pasándole como prototipo otro objeto. En el siguiente código partimos de un objeto con un par de propiedades. Usando el método getPrototypeOf() podemos obtener el prototipo a partir del cual se construyó dicho objeto. Es el prototipo básico de Object a partir del cual se construyen todos los objetos en JavaScript. Podemos tomar como prototipo ese objeto para construir otro. Veáse que ahora en getPrototypeof(otroObjeto) tenemos el objeto anterior.

//Un objeto cualquiera...
let unObjeto = {a: 1, b: 2};
console.log(Object.getPrototypeOf(unObjeto)); // Object {}
//...nos puede servir como prototipo para construir otro objeto
let otroObjeto = Object.create(unObjeto);
console.log(otroObjeto); // Object {}
console.log(Object.getPrototypeOf(otroObjeto)); // Object {a: 1, b: 2}
    

Como ya vimos, podemos crear un objeto vacío usando un literal let obj = {} o bien usar null como prototipo para hacer let obj = Object.create(null):

let obj = Object.create(null);
console.log(Object.getPrototypeOf(obj)); // null
    

De hecho el objeto nulo es el que está en la base de la cadena de prototipos. Veáse que null es un objeto y que si intentamos obtener su prototipo nos dará un error:

console.log(typeof null); // object
try {
    console.log(Object.getPrototypeOf(null));
} catch(e){
    console.log(e.message);
    // Cannot convert undefined or null to object
}
    

En el siguiente código vamos encadenando prototipos usando Object.create() con el objeto anterior. El primer objeto se creó a partir del Object base, cuyo prototipo apunta a su vez a null:

//Creamos objetos encadenando prototipos
let obj1 = {a: 1};
let obj2 = Object.create(obj1);
obj2.b = 2;
let obj3 = Object.create(obj2);
//Consultemos los prototipos
let proto3 = Object.getPrototypeOf(obj3);
let proto2 = Object.getPrototypeOf(obj2);
let proto1 = Object.getPrototypeOf(obj1);
let proto0 = Object.getPrototypeOf(proto1);
console.log(
    proto3, // Object {b: 2}
    proto2, // Object {a: 1}
    proto1, // Object {}
    proto0  // null
);
    

En el siguiente ejemplo lo podrá ver en ejecución en este navegador:

Ejemplo: Encadenando prototipos

//Creamos objetos encadenando prototipos
let obj1 = {a: 1};
let obj2 = Object.create(obj1);
obj2.b = 2;
let obj3 = Object.create(obj2);
obj3.c = 3;
        
Este ejemplo usa ES6 en modo estricto. Puedes consultar el código JS original de este ejemplo.

Este método Object.create() fue introducido en ES5 y realmente no viene a ser más que un resultado del comportamiento de las funciones como constructores de objetos. De todos los objetos intrínsecos o built-in de JavaScript, el único que posee una propiedad prototype es el objeto Function. Recuerde que todos los objetos incluidas las funciones tiene un __proto__, que es un accesor a la propiedad interna [[Prototype]]) que es el prototipo a partir del cual se construyó ese objeto.

//Los objetos que no sean funciones no tienen una propiedad prototype
console.log("abc".prototype); // undefined
console.log({a: 1}.prototype); // undefined
//Aunque todos, incluso las funciones, tienen un __proto__ que es el 
//prototipo desde el que se construyó dicho objeto
console.log("abc".__proto__); // String {length:0,[[PrimitiveValue]]:""}
console.log({a: 1}.__proto__); // Object {}
console.log((function(){}).__proto__); // function(){}
    

Pero sólo las funciones tienen además un prototype que sirve para construir nuevos objetos. Cuando las funciones construyen objetos las llamamos constructores. Por ejemplo, Object() es un constructor para construir nuevos objetos. Su prototype es un objeto vacío {}, plantilla que sirve para construir nuevos objetos. Mientras que String() es el constructor de cadenas, que tiene un objeto cadena vacía como prototipo para construir nuevas cadenas. Vea como el prototype de una función es el prototipo de Object (Object {}), plantilla igual que la del constructor Object() con la que una función como constructor construirá nuevos objetos.

//Sólo las funciones tienen un prototype
console.log(String.prototype);// String {length:0,[[PrimitiveValue]]:""}
console.log(Object.prototype);// Object {}
let fun = function(){};
console.log(fun.prototype); // Object {}
//Y eso les da la posibilidad a las funciones 
//de actuar como constructores de objetos.
//Podemos asignar cualquier objeto al prototype
fun.prototype = {a: 1, b: 2};
console.log(fun.prototype); // Object {a: 1, b: 2}
//Instanciamos objetos a partir de ese constructor
let obj = new fun();
obj.prop = "abc"
console.log(obj); // fun {prop: "abc"}
console.log(Object.getPrototypeOf(obj)); // Object {a: 1, b: 2}       
    

Se deduce que Object.create(proto) es una consecuencia de lo anterior, tal como ya había comentado en un tema de hace algún tiempo sobre que hacer cuando los navegadores no soportaban Object.create(). Se trata de declarar una función temporal, asignarle el prototipo que queríamos replicar y devolver una instancia de esa función temporal. Con lo que finalmente teníamos un nuevo objeto de igual forma que si hubiésemos usado Object.create(proto):

function create(proto){
    function temporal(){};
    temporal.prototype = proto;
    return new temporal();
}
let obj1 = {a:1, b:2};
let obj2 = create(obj1);
console.log(Object.getPrototypeOf(obj2)); // Object {a: 1, b: 2}
    

El segundo argumento de Object.create() nos permite incorporar propiedades al crear el objeto. Se trata de un objeto de descriptores de propiedades como se especifica en el método Object.defineProperty() y Object.defineProperties() que explicaremos en el siguiente tema.

let obj = Object.create(null, {
    prop1: {value: "a"},
    prop2: {value: "b"}
});
console.log(obj); // Object {prop1: "a", prop2: "b"}
    

Recuerde que no es lo mismo Object.create() que usar el constructor Object() directamente. Dado que Object() es una función puede crear cualquier clase de objeto a partir del tipo del valor que se le pase. Si le pasamos un tipo primitivo devolverá el objeto correspondiente, o mejor dicho, un objeto envoltura con el correspondiente built-in para dicho tipo primitivo. Si le pasamos un objeto lo devolverá tal cual sin afectar al prototipo.

//El constructor Object() crea objetos para cada valor
console.log(Object(123)); // Number {[[PrimitiveValue]]: 123}
//Si el valor es un objeto devuelve el mismo objeto
let obj1 = {a:1, b:2};
let obj2 = new Object(obj1);
console.log(obj1 === obj2); // true
//El prototipo siempre será el objeto básico
console.log(Object.getPrototypeOf(obj2)); // Object {}
    

Object.get/setPrototypeOf(obj), {}.isPrototypeOf(obj)

ES3/ES5/ES6
Figura
Figura. Prototipo de un objeto en JavaScript

Todo objeto tienen una propiedad denominada __proto__ que realmente sirve para acceder a la propiedad [[Prototype]] que representa otro objeto a partir del cual se construyó dicho objeto. Cuando una propiedad se escribe entre corchetes dobles es porque se trata de una propiedad interna del objeto con la que no podemos interaccionar. Realmente __proto__ es un método accesor get __proto__ que nos devuelve [[Prototype]], como se observa en la Figura.

Veníamos accediendo al prototipo con obj.__proto__, pero no había un soporte unificado entre todos los navegadores. Tampoco formaba parte de las especificaciones EcmaScript, hasta que ahora se introduce en ES6 con objeto de dar cobertura a los navegadores que ya lo soportaban. En ES5 se introdujo el método Object.getPrototypeOf(obj) para acceder al prototipo y evitar el uso de __proto__. En algunos navegadores podemos recuperar y modificar el prototipo. Para las modificaciones ahora se introduce en ES6 Objet.setPrototypeOf(obj) también para no tener que usar __proto__ (más abajo explicaremos el motivo).

En el siguiente ejemplo creamos un objeto usando otro como prototipo. El método genérico getPrototypeOf() obtiene lo mismo que usando __proto__. El método isPrototypeOf(), que viene desde ES3, nos dirá si un objeto es prototipo de otro.

//Creamos un objeto usando otro como prototipo
let unObjeto = {a: 1};
let otroObjeto = Object.create(unObjeto);
//__proto__ y getPrototypeOf nos devuelven el prototipo
console.log(Object.getPrototypeOf(otroObjeto)); // Object {a: 1}
console.log(otroObjeto.__proto__); // Object {a: 1}
//Ambos obtienen lo mismo
console.log(Object.getPrototypeOf(otroObjeto) ===
    otroObjeto.__proto__); // true
//Con isPrototypeOf consultamos si un objeto
//es prototipo de otro
console.log(unObjeto.isPrototypeOf(otroObjeto)); // true
    

Siguiendo con el código anterior, observe en el siguiente que ningún otro objeto, aunque tenga las mismas propiedades y valores, se considera prototipo. Sólo será prototipo otroObjeto que usamos en la creación de unObjeto.

let unObjetoBis = {a: 1};
console.log(unObjeto.isPrototypeOf(unObjetoBis)); // false
    

Usando __proto__ para modificar el prototipo:

let unObjeto = {a: 1}
let otroObjeto = {b: 2};
otroObjeto.__proto__ = unObjeto;
console.log(unObjeto.isPrototypeOf(otroObjeto)); // true
console.log(otroObjeto.b); // 2
    

Usando setPrototypeOf() de ES6 para modificar el prototipo:

let unObjeto = {a: 1}
let otroObjeto = {b: 2};
otroObjeto = Object.setPrototypeOf(otroObjeto, unObjeto);
console.log(unObjeto.isPrototypeOf(otroObjeto)); // true
console.log(otroObjeto.b); // 2
    

También en ES6 podemos incluir una propiedad __proto__: unObjeto, con lo que se modificará el prototipo:

let unObjeto = {a: 1};
let otroObjeto = {__proto__: unObjeto, b: 2};
console.log(unObjeto.isPrototypeOf(otroObjeto)); // true
console.log(otroObjeto.b); // 2
    

La sintaxis ha de ser necesariamente __proto__: unObjeto. Si se pasa otra cosa que no sea un objeto, la acción será ignorada. Observe que obteniendo el prototipo nos devuelve el prototipo de Object (Object {}) que es el prototipo inicial de todo objeto recién creado, por lo que la acción de modificar el prototipo a un valor que no sea un objeto no se llevó a cabo y, además, no acusó error alguno. Por otro lado vea que usamos hasOwnProperty() para verificar si un objeto posee una determinada propiedad, así que ni siquiera __proto__: "abc" fue agregada como propiedad.

let obj = {__proto__: "abc", b: 2};
console.log(obj); // Object {b: 2}
console.log(obj.__proto__); // Object {}
console.log(Object.getPrototypeOf(obj)); // Object {}
console.log(obj.hasOwnProperty("__proto__")); // false
    

Si no usamos la sintaxis __proto__: unObjeto podríamos estar creando una propiedad cualquiera con nombre "__proto__". Por ejemplo, usando los nombres de propiedad computados de ES6, forzamos la creación de una propiedad "__proto__" que no supone modificar el prototipo. Vea como ahora obj.__proto__ no accede al prototipo, sino al valor de esa propiedad, no quedando más remedio que usar getPrototypeOf().

let obj = {a: 1, ["__proto__"]: {b: 2}};
console.log(Object.getPrototypeOf(obj)); // Object {}
console.log(obj.__proto__); // Object {b: 2}
console.log(obj.__proto__.isPrototypeOf(obj)); // false
console.log(obj.hasOwnProperty("__proto__")); // true
    

Otra forma de crear una propiedad "__proto__" que no suponga modificar el prototipo es usando una notación corta para crear objetos.

let a = 1, __proto__ = {b: 2};
let obj = {a, __proto__};
console.log(Object.getPrototypeOf(obj)); // Object {}
console.log(obj.__proto__); // Object {b: 2}
console.log(obj.__proto__.isPrototypeOf(obj)); // false
console.log(obj.hasOwnProperty("__proto__")); // true
    

No hay que olvidar que los objetos se crean inicialmente con los métodos accesores get __proto__ y set __proto__. Por lo que "__proto__" no es más que el nombre de una propiedad. Y como tal podemos usar ese nombre para otro cometido. Haciéndolo así estaremos sobreescribiendo el getter y setter, pero no el prototipo, pues se ubica en la propiedad interna [[Prototype]] que no es accesible directamente. Por esa razón es preferible usar getPrototypeOf() y setPrototypeOf() para estar seguros de que realmente estamos accediendo a [[Prototype]].

Herencia con Object.create

Sobre la herencia usando Object.create() ya expuse un tema encadenando prototipos con Object.create(). Ahora vamos a recordar brevemente como Object.create() puede también servir para esto. Tenemos un constructor Persona con un método ver(). A continuación tenemos otro constructor Empleado. Hacemos que un Empleado herede propiedades y métodos de Persona haciendo Empleado.prototype = new Persona():

function Persona(nombre="???"){
    this.nombre = nombre;
}
Persona.prototype.ver = function(){
    return Object.keys(this).map(v => this[v]).join(", ");
};
function Empleado(nombre="???", profesion="???"){
    Persona.call(this, nombre);
    this.profesion = profesion;
}
Empleado.prototype = new Persona();
let uno = new Empleado("Juan", "Informático");
console.log(uno instanceof Empleado); // true
console.log(uno instanceof Persona); // true
console.log(uno.ver()); // Juan, Informático
//Existen 2 propiedades "nombre", una en la instancia 
//con valor "Juan" y otra en el prototipo de la 
//instancia con valor "???"
console.log(uno.hasOwnProperty("nombre")); // true
console.log(uno.nombre); // Juan
let proto = Object.getPrototypeOf(uno);
console.log(proto.hasOwnProperty("nombre")); // true
console.log(proto.nombre); // ???
//Si borramos en la instancia aún queda la 
//propiedad "nombre" del prototipo
delete uno.nombre;
console.log(uno.nombre); // ???
    

Esta forma clasica de implementar herencia en JavaScript tenía el problema de que se duplicaban las propiedades en ambos constructores. Con Empleado.prototype = Object.create(Persona.prototype) esto ya no sucede:

Empleado.prototype = Object.create(Persona.prototype);
let uno = new Empleado("Juan", "Informático");
console.log(uno instanceof Empleado); // true
console.log(uno instanceof Persona); // true
console.log(uno.ver()); // Juan, Informático
//Sólo existe un "nombre" en la instancia
console.log(uno.hasOwnProperty("nombre")); // true
console.log(uno.nombre); // Juan
let proto = Object.getPrototypeOf(uno);
console.log(proto.hasOwnProperty("nombre")); // false
console.log(proto.nombre); // undefined
    

Aunque lo anterior funciona bien, podemos mejorarlo si readjudicamos el constructor de la subclase Empleados, pues tal como está apuntaría a la superclase Persona. Lo hacemos con Empleado.prototype.constructor = Empleado:

Empleado.prototype = Object.create(Persona.prototype);
Empleado.prototype.constructor = Empleado;
let uno = new Empleado("Juan", "Informático");
console.log(uno.constructor); // function Empleado(){...}
    

Las nuevas clases de ES6 ofrecen una forma más fácil de implementar todo esto. Merecería una capítulo aparte, pero sólo a modo de introducción escribiremos el ejemplo anterior con las clases y subclases Persona y Empleado. Vea que el operador extends es el que hace extender la clase Persona con las propiedades y método del Empleado, en definitiva, aplicar herencia entre Persona y Empleado. Con super llamamos a la super-clase aplicando las propiedades que vayan a formar parte de ella.

class Persona {
    constructor(nombre="???"){
        this.nombre = nombre;
    }
    ver(){
        return Object.keys(this).map(v => this[v]).join(", ");
    }
}
class Empleado extends Persona {
    constructor(nombre="???", profesion="???"){
        super(nombre);
        this.profesion = profesion;
    }
}
let uno = new Empleado("Juan", "Informático");
console.log(uno instanceof Empleado); // true
console.log(uno instanceof Persona); // true
console.log(uno.ver()); // Juan, Informático
    

En fin, que si estamos pensando en usar un modelo de POO (programación orientada a objetos) con JavaScript, el uso de clases es una buena opción. Aunque no debemos olvidar que en el trasfondo del código, JavaScript sigue usando su modelo de prototipos para relacionar los objetos. Lo que vemos en la superficie con class no es más que una facilidad para que el código se parezca a lo que siempre hemos conocido como POO.

Extendiendo objetos built-in

Poder modificar el prototipo interno [[Prototype]] usando el accesor __proto__ se fundamentó principalmente en la necesidad de extender un objeto built-in. Supongamos que quiero agregar un método ver() a algunos objetos Array de una aplicación. La tarea de agregar el método a cada instancia es engorrosa, por lo que podríamos agregarlo al prototipo del built-in Array:

Array.prototype.ver = function(){return this.join("_")};
let arr = new Array(0,1,2);
console.log(arr); // [0, 1, 2]
console.log(arr.ver()); // 0_1_2
    

Pero modificar los built-in no es nada aconsejable. Por lo tanto sería interesante poder crear nuevos objetos a medida, como hacemos con los constructores pero que nos permita extender un built-in. Es decir, que un objeto pueda heredar de Array, tal como hicimos con un apartado anterior donde Empleado heredaba de Persona:

function MiArray(){
    Array.apply(this, arguments);
    //ERROR: Lo anterior no funcionó, en this
    //deberíamos tener [0, 1, 2] al instanciar
    //este ejemplo
    console.log(this); // MiArray {}
}
MiArray.prototype = Object.create(Array.prototype);
MiArray.prototype.ver = function(){return this.join("_")};
let arr = new MiArray(0,1,2);
console.log(arr instanceof Array); // true
console.log(arr instanceof MiArray); // true
//ERROR: el Array está vacío y debería ser [0, 1, 2]
console.log(arr); // MiArray {}
//Agreguemos un elemento
arr[0] = 3;
//ERROR: Agregar nuevos elementos no modifica length
console.log(arr.length); // 0
//ERROR: El método del prototipo no funciona...
console.log(arr.ver()); // ""
//...porque join() no funciona
console.log(arr.join("_")); // ""
    

El código se ejecuta sin hacer saltar error alguno. Vemos que el Array instanciado es instancia a la vez de Array y de MiArray. Pero esto es engañoso, pues hay varias cosas que van mal. Al aplicar los argumentos al constructor Array() vemos que son ignorados. En this debería haber un Array [0, 1, 2], pero sólo hay un prototipo del objeto. Otro problema es que agregar un elemento con arr[0] = 3 no modifica la propiedad length del Array. Por último el método agregado al prototipo tampoco funciona.

El intento anterior fue un auténtico desastre. Y por lo visto no había forma de extender algunos built-in que no pasara por la solución de copiar el prototipo asignando a __proto__:

//Un constructor MiArray que extiende Array
function MiArray(){
    //Al construir una instancia de este constructor
    //nos traerá el prototipo de Array en MiArray.prototype
    //y que ahora adjudicamos usando __proto__
    let temp = Array.apply(this, arguments)
    //Aquí hay un Array on los argumentos del constructor Array()
    console.log(temp); // [0, 1, 2]
    temp.__proto__ = MiArray.prototype;
    //O también ahora en ES6 con
    //Object.setPrototypeOf(temp, MiArray.prototype);
    return temp;
}
//Usamos el prototipo de Array como prototipo del constructor
//para disponer de los métodos de Array en MiArray
MiArray.prototype = Object.create(Array.prototype);
//Agregamos métodos particulares al prototipo de MiArray
//como usualmente hacíamos con los constructores de objetos
MiArray.prototype.ver = function(){return this.join("_")};
//Instanciamos un nuevo MiArray()
let arr = new MiArray(0,1,2);
console.log(arr); // [0, 1, 2]
console.log(arr instanceof Array); // true
console.log(arr instanceof MiArray); // true
arr[3] = 3;
console.log(arr); // [0, 1, 2, 3]
console.log(arr.length); // 4
console.log(arr.ver()); // 0_1_2_3
    

Y en eso se basó la necesidad de poder modificar el prototipo interno [[Prototype]] usando __proto__. También podríamos haber usado setPrototypeOf() que ahora viene en ES6. Y además con las nuevas clases de ES6 incluso ahora es más fácil hacer lo anterior:

class MiArray extends Array {
    ver(){return this.join("_")}
}
let arr = new MiArray(0,1,2);
console.log(arr instanceof MiArray); // true
console.log(arr); // [0, 1, 2]
console.log(arr.ver()); // 0_1_2
    

¿Hay que evitar modificar el prototipo?

A veces es necesario modificar el prototipo pero puede resultar una operación lenta. Esto es porque los navegadores optimizan el código para acceder a los objetos. Si modificamos el prototipo esta optimización podría no llevarse a cabo.

Hay que tener en cuenta que cuando consultamos una propiedad de un objeto, JavaScript empezará mirando en el propio objeto y luego seguirá profundizando por toda la cadena de prototipos para ver si encuentra esa propiedad. Hace tiempo que hice unas pruebas con prototipos encadenados donde se observaba que Chrome 23 no optimizaba el acceso en prototipos encadenados, mientras que Firefox 17 ya si lo hacía. Actualmente todos estos navegadores ya optimizan el acceso a propiedades y, por tanto, las modificiones en el prototipo podrían anular o disminuir el rendimiento.

Coméntemos algo ahora sobre setPrototypeOf(obj, proto) donde pasamos un objeto y un prototipo. Este método devuelve a su vez el objeto modificado, pero podría no usarse esta devolución desde que el argumento obj se pasa por referencia y la modificación se realiza en el objeto. Así en el siguiente código comprobamos que si modificamos el prototipo la acción se efectua sobre el objeto, sin que suponga crear un nuevo objeto:

//Modificando prototipo con setPrototypeOf
let obj1 = {a: 1};
let obj2 = Object.setPrototypeOf(obj1, {b: 2});
console.log(obj1 === obj2); // true
console.log(obj1); // Object {a: 1}
console.log(obj2); // Object {a: 1}
console.log(Object.getPrototypeOf(obj1)); // Object {b: 2}
console.log(Object.getPrototypeOf(obj2)); // Object {b: 2}
//Las dos variables apuntan al mismo objeto
obj2.a = 99;
console.log(obj1); // Object {a: 99}
    

Sin embargo con Object.create(obj, descriptores) siempre se creará un nuevo objeto. En el siguiente código tenemos el objeto obj1 y luego creamos otro obj2 con un prototipo, copiando los descriptores de la propiedad del primer objeto. Ahora ambas variables obj1 y obj2 apuntan a objetos distintos.

//Creando un nuevo objeto a partir de un prototipo
let obj1 = {a: 1};
let obj2 = Object.create({b: 2}, {a: 
    Object.getOwnPropertyDescriptor(obj1, "a")});
console.log(obj1 === obj2); // false
console.log(obj1); // Object {a: 1}
console.log(obj2); // Object {a: 1}
console.log(Object.getPrototypeOf(obj1)); // Object {}
console.log(Object.getPrototypeOf(obj2)); // Object {b: 2}
//Las dos variables NO apuntan al mismo objeto
obj2.a = 99;
console.log(obj1); // Object {a: 1}
    

Por lo tanto modificar el prototipo con Object.create() es una solución cuando tenemos que hacer algo con un objeto con prototipo modificado y no queremos tocar el original.

En el código anterior sólo hay una propiedad, por lo que si tuviéramos que copiar todas las propiedades deberiamos usar el metodo getOwnPropertyDescriptors(obj). Sin embargo este método no es aún soportado en ES6 estando previsto para ES 2017 (ES8). Así que podríamos hacer algo para conseguirlo:

if (!Object.hasOwnProperty("getOwnPropertyDescriptors")){
    Object.getOwnPropertyDescriptors = function(obj) {
        let opd = {};
        let keys = Object.getOwnPropertyNames(obj).
            concat(Object.getOwnPropertySymbols(obj));
        for (let key of keys){
            opd[key] = Object.getOwnPropertyDescriptor(obj, key);
        }
        return opd;
    }
}
let obj1 = {a: 1, x: 5, y: 6};
obj2 = Object.create({b: 2}, Object.getOwnPropertyDescriptors(obj1));
console.log(obj1 === obj2); // false
console.log(obj2); // Object {a: 1, x: 5, y: 6}
console.log(Object.getPrototypeOf(obj2)); // Object {b: 2}
    

Copias superficiales y profundas en un objeto

Puede parecer obvio que el hecho de que dos objetos tengan el mismo prototipo y las mismas propiedades no significará que sea el mismo objeto. Pero veámos si es así. En este código tenemos un primer objeto con un cierto prototipo. Vamos a copiar el prototipo y las propiedades en otro objeto, donde usamos getOwnPropertyDescriptors() que ya vimos en el apartado anterior:

let obj1 = {a: {x: 3}, __proto__: {b: 2}};
let obj2 = Object.create(Object.getPrototypeOf(obj1), 
    Object.getOwnPropertyDescriptors(obj1));
//El copiado es sólo superficial
console.log(obj1 === obj2); // false
//Pues los objetos más profundos no son copiados
console.log(obj1.a === obj2.a); // true
obj1.a.x = 88;
console.log(obj2.a); // Object {x: 88}
    

Sin embargo lo anterior es sólo una copia superficial (shallow copy). Los tipos primitivos si son efectivamente copiados, pero de los objetos sólo se copia la referencia que apunta a ellos. Se comprueba modificando obj1.a.x = 88 y observando que obj2.a.x también refleja ese cambio. Por lo tanto para realizar una verdadera copia hay que hacer una copia en profundidad (deep copy), que también podemos denominar clonar un objeto que veremos en un tema posterior.

Object.assign(target, source)

ES6

El método Object.assign() de ES6 realiza una copia superficial de todas las propiedades enumerables del objeto excluyendo la de su cadena de prototipos.

En el tema Copiando Arrays con destructuring pusimos un ejemplo interactivo para copiar un Array con desestructurado y otras técnicas que generaban una copia superficial:

  • let [...copia] = original
  • let copia = [].concat(original)
  • let copia = original.slice(0)
  • let copia = Object.assign([], original)
  • let copia = clonar(original);

Uno de los métodos usados era Object.assign(copia, original). Observe en el siguiente código como los valores con tipos primitivos se copian pero para los objetos lo que se copia es una referencia. Modificar un valor en un objeto de un Array se manifiesta en el otro objeto.

let original = [1, [2], {a: 3}];
let copia = Object.assign([], original);
console.log(copia); // [1, [2], {a: 3}];
copia[0] = 99;
copia[1][0] = 88;
copia[2].a = 77;
console.log(copia); // [99, [88], {a: 77}];
console.log(original); // [1, [88], {a: 77}];
    

Las propiedades del original con igual clave serán sobrescritas en la copia. Las que no existan serán creadas. En este ejemplo el valor 99 es sobrescrito y el 88 es agregado.

let original = {a: 99, c: 88};
let copia = Object.assign({a: 1, b: 2}, original);
console.log(copia); // Object {a: 99, b: 2, c: 88}
    

Se pueden pasar más de un original. En este ejemplo pasamos dos con una misma clave a. Las claves iguales van sobrescribiendo a las anteriores:

let orig1 = {a: 99};
let orig2 = {a: 88, b: 77};
let copia = Object.assign({a: 1}, orig1, orig2);
console.log(copia); // Object {a: 88, b: 77}
    

Sólo las propiedades enumerables serán copiadas:

let obj = Object.defineProperties({}, {
    "a": {value: 1, enumerable: false},
    "b": {value: 2, enumerable: true}
});
console.log(Object.assign({}, obj)); // Object {b: 2}
    

No se copiarán las propiedades en la cadena de prototipos. En este ejemplo creamos un objeto usando como prototipo {b: 2}. Esta propiedad b no es copiada con assign(), por lo que no se reliza una copia de prototipos, sino sólo a nivel de las propiedades propias del objeto:

let obj = Object.create({b: 2});
obj.a = 1;
console.log(obj); // Object {a: 1}
console.log(Object.getPrototypeOf(obj)); // Object {b: 2}
let copia = Object.assign({}, obj);
console.log(copia);  // Object {a: 1}
console.log(Object.getPrototypeOf(copia)); // Object {}
    

El ejemplo interactivo siguiente fue el que usamos en el tema Copiando Arrays con destructuring. Aunque ahora usaremos la función clonar() que expondremos en un tema posterior y que es genérica para cualquier objeto Object y no sólo para Arrays:

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.