Iterar por un objeto

Figura
Figura. Para iterar por un objeto podemos usar las claves recuperadas con el método Object.keys().

El término iterar debemos entenderlo como repetir un mismo proceso sobre los propiedades de un objeto. Podemos, por ejemplo, recorrer todas las propiedades buscando áquellas cuyos valores sean tipos string y hacer algo con esos valores.

Por otro lado tenemos el concepto de Iterables en JavaScript. Por definición los objetos como {a: 1, b: 2} no son iterables. Pero un Array como [1, 2] si lo es. Los iterables poseen un método iterador Symbol.iterator() que posibilita la iteración en determinadas circunstancias. Ese método no viene por defecto cuando creamos un nuevo objeto con las técnicas para inicializar un objeto.

Para iterar por un objeto hemos de recurrir a sus propios métodos que expondremos en este tema. Como todos los objetos derivan de Object estos métodos también se pueden aplicar a ellos, junto a la posibilidad de que implementen también el método Symbol.iterator().

{}.hasOwnProperty(prop)

ES3

El método del prototipo {}.hasOwnProperty(prop) nos dice si un objeto contiene una propiedad propia que no pertenezca a la cadena de prototipos. Lo hemos incluido en este tema pues nos sirve para filtrar propiedades cuando iteremos por ellas. En el siguiente ejemplo usamos Objet.create() para crear el objeto {b: 2} y poniéndole el prototipo {a: 1}. El bucle for in recuperará todas las propiedades, propias y de la cadena de prototipos.

let obj = Object.create({a: 1}, {b: {value: 2, enumerable: true}});
console.log(obj); // Object {b: 2}
console.log(Object.getPrototypeOf(obj)); // Object {a: 1}
let cad = "";
for (let prop in  obj) {
    if (cad!=="") cad += ", ";
    cad += `${prop}: ${obj[prop]}`;
}
console.log(cad); // b: 2, a: 1
    

Observe que la propiedad b tiene el descriptor enumerable activado, pues en otro caso no sería recuperable con el for in. Para filtrar sólo las propiedades propias podemos usar obj.hasOwnProperty(prop):

cad = "";
for (let prop in  obj) {
    if (obj.hasOwnProperty(prop)){
        if (cad!=="") cad += ", ";
        cad += `${prop}: ${obj[prop]}`;
    }
}
console.log(cad); // b: 2
    

Bucles for in e iterables con bucles for of

El bucle for in busca propiedades sólo enumerables en la cadena de prototipos. Es la única forma de iterar por todas las propiedades de la cadena de prototipos de un objeto sin necesidad de acceder a los objetos prototipo de esa cadena. Pero si existieran nombres de propiedades repetidos sólo se recuperará el primero de ellos. En el siguiente ejemplo la propiedad a existe en el objeto y en el prototipo. Sólo se recupera la primera.

let arr = {a: 1, b: 2, __proto__: {a: 99, c: 3}};
let cad = "";
for (let prop in arr){
    if (cad!=="") cad += ", ";
    cad += `${prop}: ${arr[prop]}`;
}
console.log(cad); // a: 1, b: 2, c: 3
    

Recuerde del tema acerca de iterables que los objetos de Object no son iterables, pues no disponen de un método iterador.

let arr = [1, 2];
console.log(arr[Symbol.iterator]); // function values() {..}
let obj = {a: 1, b: 2};
console.log(obj[Symbol.iterator]); // undefined
    

Un objeto Array por ejemplo es iterable y puede ser fuente de un bucle for of, pero no un objeto:

//Un Array es iterable
let arr = [1, 2];
let cad = "";
for (let item of arr){
    if (cad!=="") cad += ", ";
    cad += item;
}
console.log(cad); // 1, 2
//Pero un objeto no lo es
try {
    let obj = {a: 1, b: 2};
    for (let item of obj){
        if (cad!=="") cad += ", ";
        cad += item;
    }
    console.log(cad); 
} catch(e){
    console.log(e.message); // obj[Symbol.iterator] is not a function
}
    

En el tema de iterables vimos que podemos hacer iterable un objeto si posee claves numéricas correlativas desde cero, una propiedad length con el total de elementos y un método iterador:

//Un objeto puede ser iterable si tiene claves numéricas,
//una propiedad length y un método Symbol.iterator
let obj = {
    0: "a", 
    1: "b",
    length: 2,
    [Symbol.iterator]: function(){
        let index = 0, obj = this;
        return {
            next: function(){
                if (index < obj.length){
                    return {value: obj[index++], done: false};
                } else {
                    return {value: undefined, done: true}
                }
            }
        };
    },
    //Esta propiedad no es numérica y no será iterada
    c: "c"
};
console.log(typeof obj[Symbol.iterator]); // function
let cad = "";
for (let item of obj){
    if (cad!=="") cad += ", ";
    cad += item;
}
console.log(cad); // a, b
    

Con una función generadora también podemos construir un iterador para un objeto. En lugar de un bucle for of hemos usado el operador de propagación que también usa como fuente un iterable. Observe, como antes, que la propiedad c no es numérica y por tanto no será recuperada:

let obj = {
    0: "a", 
    1: "b",
    length: 2,
    [Symbol.iterator]: function*(){
        let index = -1;
        while (index++, index < this.length){
            yield this[index];
        }
    },
    //Esta propiedad no es numérica y no será iterada
    c: "c"
};
console.log(typeof obj[Symbol.iterator]); // function
console.log([...obj]); // ["a", "b"]
    

{}.propertyIsEnumerable(prop)

ES3

El método del prototipo {}.propertyIsEnumerable(prop) nos dice si una propiedad es enumerable. Podríamos saberlo también consultando el descriptor:

let obj = Object.defineProperties({}, {
    "a": {value: 1, enumerable: true},
    "b": {value: 2, enumerable: false}
});
console.log(obj.propertyIsEnumerable("a")); // true
console.log(obj.propertyIsEnumerable("b")); // false
//Obtenemos lo mismo con getOwnPropertyDescriptor
console.log(Object.getOwnPropertyDescriptor(obj, "a").enumerable);
    // true
console.log(Object.getOwnPropertyDescriptor(obj, "b").enumerable);
    // false
    

Pero hay algo importante a tener en cuenta con propertyIsEnumerable(), pues sólo busca en las propiedades del objeto y no en la cadena de prototipos. Si no encuentra una propiedad propia devolverá falso. En el siguiente ejemplo {b: 2} es el objeto y su prototipo es {a: 1}. Ambas propiedades a y b son enumerables, pero con la del prototipo el método propertyIsEnumerable() nos devolverá falso pues no la encontró en el objeto:

let obj = Object.create({a: 1}, {b: {value: 2, enumerable: true}});
//"b" es una propiedad enumerable del objeto
console.log(obj); // Object {b: 2}
console.log(obj.propertyIsEnumerable("b")); // true
//"a" es una propiedad enumerable del prototipo
let proto = Object.getPrototypeOf(obj); 
console.log(proto); // Object {a: 1}
console.log(proto.propertyIsEnumerable("a")); // true
//Sin embargo propertyIsEnumerable nos dará falso porque
//no busca en la cadena de prototipos
console.log(obj.propertyIsEnumerable("a")); // false
    

Object.keys(obj)

ES5

El método genérico Object.keys(obj) nos devuelve un Array de las propiedades propias enumerables. En el siguiente código hemos agregado una propiedad c con el método defineProperty() que por defecto establece las propiedades como no enumerables. Esta propiedad c así como d del prototipo no serán recuperadas por Object.keys():

let obj = {a: 1, b: 2, __proto__: {d: 4}};
obj = Object.defineProperty(obj, "c", {value: 3});
console.log(obj); // Object {a: 1, b: 2, c: 3}
console.log(Object.keys(obj)); // ["a", "b"]
    

El Array de claves se entrega en el mismo orden que se definió en el objeto para las claves que no sean números enteros no negativos, pues éstas se entregan ordenadas:

let obj = {z: "zeta", 6: "seis", n: "ene", 3: "tres"}
console.log(Object.keys(obj)); // ["3", "6", "z", "n"]
    

El hecho de que keys() nos devuelva un Array nos puede servir para usarlo como fuente de un bucle for of:

let obj = {a: 1, b: 2};
let cad = "";
for (let key of Object.keys(obj)){
    if (cad!=="") cad += ", ";
    cad += `${key}: ${obj[key]}`;
}
console.log(cad); // a: 1, b: 2
    

O incluso métodos que iteran por un Array como forEach() y otros como reverse(), map(), reduce() o some():

let obj = {a: 1, b: 2};
//Método forEach() para iterar con un callback
let cad = "";
Object.keys(obj).forEach(key => {
    if (cad!=="") cad += ", ";
    cad +=`${key}: ${obj[key]}`
});
console.log(cad); // a: 1, b: 2
//Método reverse() para ordenarlo invertido
console.log(Object.keys(obj).reverse()); // ["b", "a"]
//Método map() para iterar devolviendo nuevo Array
console.log(Object.keys(obj).map(key => 
    `${key}: ${obj[key]}`).join(", ")); // a: 1, b: 2
//Método reduce() para reducir a un valor simple
console.log(Object.keys(obj).reduce((p, key) => 
    `${p}${(p)?", ":""}${key}: ${obj[key]}`, "")); // a: 1, b: 2
//Método some() para comprobar una condición con las claves
console.log(Object.keys(obj).some(key => key=="b")); // true
    

Las propiedades declaradas como accesores getter o setter también se recuperan con keys():

let x = 0, y = 0;
let obj = {
    a: 1,
    get b(){return x},
    set c(val){y = val}
};
let keys = Object.keys(obj); 
console.log(keys); // ["a", "b", "c"]
    

El Array de claves obtenido no diferencia entre getters y setters. Aunque no es usual poner sólo un setter en una propiedad, en el del ejemplo vemos que no hay nada que recuperar en c y es por lo que devolverá undefined:

console.log(Object.keys(obj).reduce((p, key) => 
    `${p}${(p)?", ":""}${key}: ${obj[key]}`, "")); 
    // a: 1, b: 0, c: undefined
    

Object.values(obj)

ES8

Si Object.keys(obj) nos devolvía un Array con las claves de las propiedades propias enumerables, el método Object.values(obj) nos devolverá los valores. Es un método previsto para ES8 (2017). Actualmente es soportado parcialmente en Firefox 48 pero no directamente en Chrome 53. Se debe tener en cuenta esto, puesto que el comportamiento definitivo del método podría cambiar. En general no deberíamos usar en producción características que no formen parte de un estándar.

let obj = {a: 1, b: 2};
console.log(Object.keys(obj)); // ["a", "b"]
try {
    console.log(Object.values(obj)); // [1, 2]
} catch(e){
    //Chrome 53 no lo soporta directamente
    console.log(e.message); // Object.values is not a function
}
    

El iterador predeterminado de un Array es la función values(). El prototipo de Array dispone de un método [].values() que nos devuelve un objeto iterador con los valores del Array:

let arr = [1, 2, 3];
console.log(arr[Symbol.iterator]); // function values(){..}
try {
    //En Firefox 47 obtenemos los valores que podemos 
    //propagar en un Array por ejemplo
    console.log([...arr.values()]); // [1, 2, 3]
} catch(e){
    //En Chrome 53 hay un error con arr.values()
    console.log(e.message); // arr.values(...)[Symbol.iterator] 
                            // is not a function
}
    

Chrome 53 parece que nos dice que soporta un Symbol.iterator para un Array apuntando a una función values(), pero realmente no reconoce el método values() por ahora.

Las propiedades declaradas como accesores getter o setter también se recuperan con values(). Los getter son ejecutados en ese momento para obtener su valor. Usualmente ponemos un getter y un setter para una misma propiedad. Pero la propiedad c del siguiente ejemplo es sólo un setter y no hay nada que recuperar, por lo que obtendremos un undefined:

let x = 0, y = 0;
let obj = {
    a: 1,
    get b(){return x},
    set c(val){y = val}
};
console.log(Object.values(obj)); // [1, 0, undefined]
    

Object.entries(obj)

ES8

El método genérico Object.entries(obj) nos devuelve un Array de parejas [clave, valor] de todas las propiedades propias enumerables. Dígamos que integra en una única acción Object.keys() y Object.values(). Es un método previsto para ES8 (2017). Actualmente es soportado parcialmente en Firefox 48 pero no directamente en Chrome 53.

let obj = {a: 1, b: 2};
try{
    let entradas = Object.entries(obj);
    console.log(entradas); // [["a", 1], ["b", 2]]
} catch(e){
    //Chrome 53 no soporta directamente entries()
    console.log(e); // Object.entries is not a function
}
    

Un Array tiene un metodo [].entries() del prototipo. Devuelve un particular objeto iterador ArrayIterator que podemos convertir en un Array con parejas clave-valor:

let arr = ["a", "b"];
let entradas = arr.entries();
console.log(entradas); // ArrayIterator {}
console.log([...entradas]); // [[0, "a"], [1, "b"]]
    

Una ventaja de entries() es que podemos usar destructuring en el destino de un for of. Aquí vemos que poniendo [key, val] iteramos por todas las parejas clave-valor del objeto, haciendo más fácil la recuperación de valores:

let obj = {a: 1, b: 2};
let cad = "";
for (let [key, val] of Object.entries(obj)){
    if (cad!=="") cad += ", ";
    cad +=`${key}: ${val}`;
}
console.log(cad); // a: 1, b: 2
    

El nuevo objeto built-in Map es un mapa que representa un conjunto de parejas clave-valor. Un mapa no es un Object ni un Array, es una nueva estructura. Además por ahora no puede declararse literalmente. Los navegadores no se han puesto de acuerdo para representarlo en la consola, como se observa en este ejemplo donde se construye un mapa pasando al constructor un Array de Arrays con dos posiciones clave-valor:

let mapa = new Map([["a", 1], ["b", 2]]);
console.log(mapa); // Map {"a" => 1, "b" => 2} en Chrome
                   // Map {a: 1, b: 2} en Firefox
console.log(mapa[Symbol.iterator]); // function entries() {..}
    

A diferencia de un objeto, un mapa es iterable. Observe como el iterador predeterminado de un mapa es una función entries(). Por eso el método Object.entries(obj) nos permite convertir fácilmente en mapa un objeto:

let obj = {a: 1, b: 2};
let mapa = new Map(Object.entries(obj));
console.log(mapa); // Map {"a" => 1, "b" => 2} en Chrome
                   // Map { a: 1, b: 2 } en Firefox
    

Object.getOwnPropertyNames(obj)

ES5

El método Object.getOwnPropertyNames(obj) nos devuelve un Array con las claves de las propiedades propias enumerables y no enumerables de un objeto. No recupera la de su cadena de prototipos. En el siguiente ejemplo verá que se recuperan las claves enumerables a y b propias, pero no la c del prototipo:

let obj = Object.create({c: 1}, {
    a: {value: 2, enumerable: true},
    b: {value: 3, enumerable: false},
    [Symbol.for("c")]: {value: 3, enumerable: true}
});
console.log(Object.keys(obj)); // ["a"]
console.log(Object.getOwnPropertyNames(obj)); // ["a", "b"]
    

Vea que tampoco recupera las claves con símbolos, para lo cual debemos usar getOwnPropertySymbols() que veremos en un siguiente apartado. Por otro lado para recuperar las claves del prototipo tendríamos que usar getPrototypeOf() para obtener sus claves. Luego podemos concatenarlas con el Array de claves del objeto:

let obj = Object.create({c: 1}, {
    a: {value: 2, enumerable: true},
    b: {value: 3, enumerable: false}
});    
let proto = Object.getPrototypeOf(obj);
let keysProto = Object.getOwnPropertyNames(proto);
let keysObj = Object.getOwnPropertyNames(obj);
console.log(keysObj.concat(keysProto)); // ["a", "b", "c"]
    

Lo anterior sólo recupera las claves superficiales del prototipo. Si la cadena fuera más profunda tendríamos que usar algo para extraer los prototipos y sus claves:

let obj = {
    a: 1,
    __proto__: {
        b: 3,
        __proto__: {
            c: 4,
            __proto__: {
                d: 5
            }
        }
    }
};
let keys = [], temp = obj;
while (!temp.hasOwnProperty("constructor")){
    keys = keys.concat(Object.getOwnPropertyNames(temp));
    temp = Object.getPrototypeOf(temp);
}
console.log(keys); // ["a", "b", "c", "d"]
    

Un objeto siempre tendrá al final de la cadena el prototipo de Object {} que sirve de prototipo para todos los objetos de JavaScript. Y a su vez el prototipo de ese prototipo de Object será el objeto null, punto en el cual finaliza la cadena de prototipos. Esto lo puede consultar en el ejemplo interactivo donde explicábamos las funciones como constructores. Así que en el código anterior podríamos poner como condición de finalización temp !== null, pero en ese caso recuperaría las propiedades del prototipo de Object Object {}. Una propiedad característica del prototipo de Object es constructor. Por lo tanto preguntamos si el objeto en la cadena de prototipos tiene una propiedad propia constructor para omitir ese objeto en la búsqueda de propiedades.

Lo anterior nos sirve para iterar por la cadena de prototipos y obtener sus claves. Otra cosa diferente sería iterar por un objeto para hacer algo con sus valores, como representarlos textualmente. Si los valores son a su vez objetos tendremos que iterar otra vez por ellos de forma recursiva. En el siguiente ejemplo recorremos todos los valores de un objeto. Si un valor es un objeto lo recorremos recursivamente. También es posible ver la cadena de prototipos.

function ver(obj){
    let cad = "";
    if (obj !== null && typeof obj === "object")){
        let keys = Object.getOwnPropertyNames(obj);
        for (let key of keys){
            let val = obj[key];
            if (typeof val === "string") val = `"${val}"`;
            if (obj[key] !== null && typeof obj[key] === "object"){
                cad += ((cad!=="")?",":"") + 
                ((obj.constructor.name==="Array")?"":`"${key}":`) +
                ver(obj[key]);
            } else if ((obj.constructor.name==="Array")){
                if (/\d+/.test(key)) {
                    cad += ((cad!=="")?",":"") + val;
                }
            } else {
                cad += ((cad!=="")?",":"") + `"${key}":` + val;
            }
        }
        /* Si quisiéramos los prototipos agregaríamos esto
        let proto = Object.getPrototypeOf(obj);
        if (proto !== null && !proto.hasOwnProperty("constructor")) {
            cad += ((cad!=="")?",":"") + '__proto__: ' + ver(proto);
        }*/
    }
    let [car1, car2] = ((obj.constructor.name==="Array"))?
                       ["[", "]"]:
                       ["{", "}"];
    return car1 + cad + car2;
}
let obj = {
    a: 1,
    x: {z: 2, m: 3},
    y: [4, null, {w: 5, v: [6, 7]}]
};
console.log(ver(obj));
// {"a":1,"x":{"z":2,"m":3},"y":[4,null,{"w":5,"v":[6,7]}]}
console.log(JSON.stringify(obj));
// {"a":1,"x":{"z":2,"m":3},"y":[4,null,{"w":5,"v":[6,7]}]}
    

Si usamos el método del buil-int JSON.stringify(obj) obtenemos una cadena con el objeto en notación JSON. En esta notación todas las propiedades deben ir entrecomilladas y lo valores deben ser tipos primitivos String, Number, Boolean o null o bien objetos o Arrays con estos tipos primitivos. Cualquier otra cosa se convertirá a null o será ignorada. Observe en el ejemplo como nuestro método ver() extrae lo mismo que ese JSON.stringify().

No use ese ejemplo con la función ver() para convertir un objeto a JSON, pues para eso está precisamente ese método JSON.stringify(). Sólo he puesto ambos para compararlos y explicar que podemos iterar por un objeto para hacer algo con los valores.

El código anterior con algunas mejoras sirve para el siguiente ejemplo interactivo. Podemos incluir una propiedad con un símbolo por clave, pues utilizamos el método getOwnPropertySymbols() para recuperarlos como veremos en el siguiente apartado. También podemos incluir dos propiedades con valores Set y Map que representamos con corchetes Set [] y Map [], pero no hay que olvidar que no son Arrays.

Ejemplo: Iterar por un objeto

let objeto = {
    a: 1,
    b: [true, {c: "X", d: [2, 3]}],
    e: {f: "Y", g: null}
};
        
ver(objeto)
JSON.stringify(objeto)
Este ejemplo usa ES6 en modo estricto. Puedes consultar el código JS original de este ejemplo.

Object.getOwnPropertySymbols(obj)

ES6

Los Símbolos fueron introducidos en ES6 aunque no se modificó getOwnPropertyNames() de ES5 para extraer también las propiedades con claves de símbolos. Para ese cometido se creó el nuevo método getOwnPropertySymbols() que recupera sólo las claves con símbolos. En este ejemplo usamos el método de Array concat() para concatenar los dos resultados, extrayendo propiedades y símbolos propios enumarables y no enumerables de un objeto:

let obj = Object.create({c: 1}, {
    a: {value: 2, enumerable: true},
    b: {value: 3, enumerable: false},
    [Symbol.for("c")]: {value: 3, enumerable: true}
});
console.log(Object.keys(obj)); // ["a"]
//Concatenamos nombres y símbolos
console.log(Object.getOwnPropertyNames(obj).
concat(Object.getOwnPropertySymbols(obj))); // ["a", "b", Symbol(c)]
    

Clonar un objeto

En un tema anterior comentamos acerca del método getOwnPropertyDescripts() que está previsto que forme parte de ES7 (2018). Por ahora podíamos usar algo como lo siguiente para conseguir lo mismo, donde extraemos los nombres de propiedades y símbolos:

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;
    }
}
    

Ese método nos servía para obtener los descriptores de un objeto. Haciendo uso de getPrototypeOf() y de create() podemos clonar un objeto realizando una copia en profundidad:

function clonar(obj){
    let clon = obj;
    if (obj !== null && typeof obj === "object"){
        let proto = Object.getPrototypeOf(obj);
        if (!proto.hasOwnProperty("constructor")) proto = clonar(proto);
        let valores = Object.getOwnPropertyDescriptors(obj);
        clon = Object.create(proto, valores);
        let keys = Object.getOwnPropertyNames(obj).
            concat(Object.getOwnPropertySymbols(obj));
        for (let key of keys){
            if (obj[key] !== null && typeof obj[key] === "object") {
                clon[key] = clonar(obj[key]);
            }
        }
    }
    return clon;
}
    

Vemos que primero obtenemos el prototipo del objeto. Toda cadena de prototipos tendrá al menos uno que viene de un constructor y que no debe ser clonado pues es el prototipo del constructor que comparten los objetos del mismo origen. Necesitamos obtener los descriptores de los valores para usarlo en Object.create() y crear una copia superficial del objeto. Luego iteramos por los nombres de propiedades y símbolos propios enumerables y no enumerables para, a su vez, clonar los valores que sean objetos.

El objeto del siguiente código es clonado con la función anterior. A continuación modificamos algunos valores númericos en el clon y observamos que no se modifican en el original. Ambos objetos son efectivamente distintos en toda su profundidad.

let objeto = {
    a: 1,
    b: [true, {c: "X", d: [2, 3]}],
    e: {f: "Y", g: null, __proto__: {h: 4}},
    __proto__: {
        i: {j: 5},
        __proto__: {k: [6, 7]}
    }
};
let clon = clonar(objeto);
//Modificamos algunos valores en el clon
clon.a += 100;
clon.b[1].d[0] += 100;
clon.e.__proto__.h += 100;
clon.__proto__.__proto__.k[0] += 100;
console.log(ver(clon));
// {"a":101,
// "b":[true,{"c":"X","d":[102,3]}],
// "e":{"f":"Y","g":null,__proto__: {"h":104}},
// __proto__: {"i":{"j":5},__proto__: {"k":[106,7]}}}
//El original no se modifica
console.log(ver(objeto));
// {"a":1,
// "b":[true,{"c":"X","d":[2,3]}],
// "e":{"f":"Y","g":null,__proto__: {"h":4}},
// __proto__: {"i":{"j":5},__proto__: {"k":[6,7]}}}
    

Hemo usado la función ver(objeto) que expusimos en un apartado anterior. En el siguiente ejemplo lo puede ver en ejecución en este navegador:

Ejemplo: Clonando objetos

Crear objeto
let objeto = {
    a: 1,
    b: [true, {c: "X", d: [2, 3]}],
    e: {f: "Y", g: null, __proto__: {h: 4}},
    __proto__: {
        i: {j: 5},
        __proto__: {k: [6, 7]}
    }
};
            
function A(a){
    this.a = a;
}
function B(a, b){
    A.call(this, a);
    this.b = b;
}
B.prototype = Object.create(A.prototype, 
    {c:{value: {z:3}, enumerable: true}});
B.prototype.constructor = B;
let objeto = new B({x: 1}, {y: 2});
            
class AA {
    constructor(a){
        this.a = a;
    }
}
class BB extends AA {
    constructor(a, b){
        super(a);
        this.b = b;
    }
}
BB.prototype.c = {z: 3};
let objeto = new BB({x: 1}, {y: 2});
            

En la siguiente acción clonaremos ese objeto, modificaremos valores numéricos en el clon y, finalmente, visualizaremos ambos usando la función ver() que expusimos en un apartado anterior:

ver(objeto)
ver(clon)
Este ejemplo usa ES6 en modo estricto. Puedes consultar el código JS original de este ejemplo.

Usando un constructor o las nuevas clases de ES6, vemos que el prototipo del constructor no es clonado. La propiedad c: {z: 3} se modificará al valor {z: 103} en la copia y también en el original. Seguir el criterio de que las propiedades del prototipo constructor no deben clonarse se fundamenta en que el objetivo es que estas propiedades sean compartidas por todas las instancias. Aunque este criterio podría modificarse simplemente eliminando el condicional if (!proto.hasOwnProperty("constructor")) en el código de la función clonar().

Clonar objetos funcionará con Arrays y Objetos normales, pero no con Set o Map. Si clonamos un objeto con esos built-in vemos que no se están copiando:

let objeto = {
    a: 1,
    m: new Set([0, 1]),
    n: new Map([["p", 0],["q", 1]]),
};
console.log(objeto.m); // Set {0, 1}
console.log(objeto.n); // Map {"p" => 0, "q" => 1}
let clon = clonar(objeto);
console.log(clon); // Object {a: 1, m: Set, n: Map}
//Set y Map no resultaron copiados
console.log(clon.m); // Set {}
console.log(clon.n); // Map {}
    

Una solución podría usar el propio constructor para crear un copia del objeto, pero como vemos en el siguiente código esa copia no es profunda:

let s = new Set([{a: 1}]);
console.log(s); // Set {Object {a: 1}}
let t = new Set(s);
console.log(t); // Set {Object {a: 1}}
console.log(s===t); // false
//Cambiamos el valor en el Set copia
for (let item of t){
    item.a = 2;
}
console.log(t); // Set {Object {a: 2}} 
//Y vemos que se cambia en el original
console.log(s); // Set {Object {a: 2}}
    

Espero estudiar más a fondo los objetos Set y Map para entender su comportamiento.