Cloning and merging objects
Copying variables
Earlier, when we went over value data types and reference data types and over passing object arguments to functions, we already handled some specifics regarding copying variables (or constants or properties) with object values.
When copying an variable (or constant or property) with an object value, the object value will not be copied but a reference to the same object value will be copied. Both variables will share a reference to the same object value. This is sometimes referred to as "pass by sharing" or "copy of reference". Objects are of the reference data type, meaning that, unlike with primitive values, the variable does not hold the value itself, but it holds a reference to an object value that is stored at a separate memory location. Multiple variables can reference the same object value, which is not possible with primitives. You can modify an object by referring to any original or copied variable that points to that object.
const objectVar = { a: 2 };
const objectVarCopy = objectVar;
// Two variables, one single object,
// both objects are pointing to same object:
console.log(objectVar === objectVarCopy); // logs: true
// Changing the object changes it for both variables:
objectVar.a = 20;
console.log(objectVarCopy.a); // logs: 20
objectVarCopy.a = 200;
console.log(objectVar.a); // logs: 200
In JavaScript, next to assignment, also comparison (equality) is value based for primitives and reference based for objects, as we have seen earlier this tutorial.
console.log(1 === 1); // logs: true
console.log({} === {}); // logs: false
Object.assign()
and for...in
If you want to create a new variable referring to a separate object value that actually is a copy of an other object value,
you will need to clone the object value first.
JavaScript provides several ways to clone an object value, you can do this by using a for...in
loop
or Object.assign()
. Also spread syntax, structuredClone()
and JSON serializing can be used to clone objects, which will be explained later in this chapter.
Object.assign()
:
const myObj = { a: 1, b: 2, c: 3 };
const copyMyObj = Object.assign({}, myObj);
copyMyObj.a = 10;
console.log(myObj); // logs: { a: 1, b: 2, c: 3 }
console.log(copyMyObj); // logs: { a: 10, b: 2, c: 3 }
for...in
:
const myObj = { a: 1, b: 2, c: 3 };
const copyMyObj = {};
for (let propertyName in myObj) {
if (Object.hasOwn(myObj, propertyName)) {
copyMyObj[propertyName] = myObj[propertyName];
}
}
copyMyObj.a = 10;
console.log(myObj); // logs: { a: 1, b: 2, c: 3 }
console.log(copyMyObj); // logs: { a: 10, b: 2, c: 3 }
The Object.assign()
method copies only all enumerable own properties from one or more source objects to a target object
(the empty object {}
in the examples). It returns the modified target object.
You can also use this method to merge several objects into one object.
const source1 = { a: 1, b: 2, c: 3 };
const source2 = { c: 4, d: 5, e: 6 };
const target = Object.assign({}, source1, source2);
console.log(target); // logs: { a: 1, b: 2, c: 4, d: 5, e: 6 }
Be aware that both for...in
and
Object.assign()
will not copy getters and setters.
Instead it will invoke the source's property getter and assigns the obtained property's value to the target property.
const person = {
get name() {
return this._name;
},
set name(value) {
value = value.trim();
if (value === '') { return }
this._name = value;
}
}
person.name = "John Doe";
const copyPerson = Object.assign({}, person);
console.log(person); // logs: { name: Getter & Setter, _name: "John Doe" }
console.log(copyPerson); // logs: { name: "John Doe", _name: "John Doe" }
Also mind that both create a "shallow clone", meaning that if an object property again is a reference to an object value, both the original and the copy share the same property value. Also, they do not copy possible prototypes and they only copy properties if they are enumerable.
for...in
copies both own and inherited properties, while Object.assign()
only copies own properties.
Unlike for...in
, Object.assign()
also copies
symbol keyed properties, which will be explained in the next chapter.
const myObj = { a: 1, b: 2, c: { d: 3 }, __proto__: { e: 4 } };
const copyMyObj = Object.assign({}, myObj);
copyMyObj.a = 10;
copyMyObj.c.d = 30;
console.log(myObj); // logs: { a: 1, b: 2, c: { d: 30 } }
console.log(copyMyObj); // logs: { a: 10, b: 2, c: { d: 30 } }
console.log(Object.getPrototypeOf(myObj) === Object.prototype); // logs: false
console.log(Object.getPrototypeOf(copyMyObj) === Object.prototype); // logs: true
const copyMyObj2 = {};
Object.defineProperty(myObj, 'f', {
enumerable: false,
value: 5
});
for (let propertyName in myObj) {
copyMyObj2[propertyName] = myObj[propertyName];
}
console.log(copyMyObj2); // logs: { a: 1, b: 2, c: { d: 30 }, e: 4 }
console.log(myObj.f); // logs: 5
In the chapter about recursion an example showed how to traverse an object with nested objects. It copied all properties, including the properties of nested objects, into a non-nested object (which is not the same as "deep cloning", as will be explained below).
Spread syntax
We covered spread syntax before, to apply all elements from an iterable object, like an array, one-by-one in a function call's arguments list. We can also spread properties from a provided object into a new object. This way we can create a shallow copy of an object, without copying its prototypes. Unlike with spreading in function parameters (using the object's iterator), spreading in object literals enumerates the own properties of the object. Therefore, it copies only own enumerable properties (in an arbitrary order). It also copies own enumerable symbol keyed properties, which will be explained in the next chapter.
const myObj = { a: 1, b: 2 };
const copyMyObj = { ...myObj };
console.log(copyMyObj); //logs: { a: 1, b: 2 }
const composedObj = { ...myObj, a: 0, c: 3 };
console.log(composedObj); //logs: { a: 0, b: 2, c: 3 }
// all array's elements are enumerable own properties,
// so arrays can be spread into objects:
const myArray = ["a", "b"];
const myArrayInObj = { ...myArray };
console.log(myArrayInObj); //logs: { 0: "a", 1: "b" }
Spread properties can also be used to merge several objects into one object:
const myObj1 = { a: 1, b: 2, c: 3 };
const myObj2 = { c: 4, d: 5, e: 6 };
const mergedObj = {...myObj1, ...myObj2};
console.log(mergedObj); //logs: { a: 1, b: 2, c: 4, d: 5, e: 6 }
Spread syntax provides a shorter syntax than Object.assign()
for
shallow-cloning or merging objects. Next a final example:
let myObj = { a: 1, b: 2 };
const mergedObj1 = Object.assign({}, myObj, { a: "Hello" });
console.log(mergedObj1); // logs: { a: "Hello", b: 2 }
const mergedObj2 = {...myObj, ...{ a: "Hello" }};
console.log(mergedObj2); //logs: { a: "Hello", b: 2 }
Object.assign(myObj, { c: 3, d: 4 });
console.log(myObj); //logs: { a: 1, b: 2, c: 3, d: 4 }
myObj = {...myObj, ...{ e: 5 }}
console.log(myObj); //logs: { a: 1, b: 2, c: 3, d: 4, e: 5 }
Object destructuring and rest properties
Also with object destructuring (will be covered later) and rest properties you can create a (altered) shallow copy. It does not copy its prototypes, it copies only own enumerable properties and it also copies symbol keyed properties, which will be explained in the next chapter.
const myObject = { prop1: 1, prop2: 2, prop3: 3 };
const {...copiedObject} = myObject;
console.log(copiedObject); // logs: { prop1: 1, prop2: 2, prop3: 3 }
Copy, except one property:
const myObject = { prop1: 1, prop2: 2, prop3: 3 };
const {prop2, ...copiedObject} = myObject;
console.log(copiedObject); // logs: { prop1: 1, prop3: 3 }
Deep cloning
To create a "deep clone" of an object, that is, creating a clone of an object in which also all nested objects are cloned,
you can use the global structuredClone()
method.
However, also with structuredClone()
, only enumerable own properties are copied, prototypes are not copied
and getters and setters are not copied.
const myObj = { a: 1, b: 2, c: { d: 3 }, __proto__: { e: 4 } };
Object.defineProperty(myObj, 'f', {
enumerable: false,
value: 5
});
const copyMyObj = structuredClone(myObj);
copyMyObj.a = 10;
copyMyObj.c.d = 30;
console.log(myObj); // logs: { a: 1, b: 2, c: { d: 3 }, f: 5 }
console.log(copyMyObj); // logs: { a: 10, b: 2, c: { d: 30 } }
console.log(Object.getPrototypeOf(myObj) === Object.prototype); // logs: false
console.log(Object.getPrototypeOf(copyMyObj) === Object.prototype); // logs: true
Alternatively you can use the JSON.stringify()
and JSON.parse()
methods of the JavaScript built-in
JSON object to create a deep clone.
The JSON object contains methods for parsing JSON and converting values to JSON.
JSON is an abbreviation for JavaScript Object Notation,
which is an open standard data format (derived from JavaScript) to store and transmit data and supported by many modern programming languages.
const myObj = { a: 1, b: 2, c: { d: 3 }, __proto__: { e: 4 } };
Object.defineProperty(myObj, 'f', {
enumerable: false,
value: 5
});
const copyMyObj = JSON.parse(JSON.stringify(myObj));
copyMyObj.a = 10;
copyMyObj.c.d = 30;
console.log(myObj); // logs: { a: 1, b: 2, c: { d: 3 }, f: 5 }
console.log(copyMyObj); // logs: { a: 10, b: 2, c: { d: 30 } }
console.log(Object.getPrototypeOf(myObj) === Object.prototype); // logs: false
console.log(Object.getPrototypeOf(copyMyObj) === Object.prototype); // logs: true
Also with JSON.stringify()
, only enumerable own properties are copied, prototypes are not copied
and getters and setters are not copied.
There are issues with deep copying objects using the JSON methods. Any JavaScript object that cannot be serialized to a JSON string will be lost or altered in the conversion.
For instance, properties with function values (methods) will not be copied.
The structuredClone()
method has a little less issues, but also this method does not work
with functions: it'll throw an error. Check this
MDN page about the structured clone algorithm
for what works and what doesn't (performance may improve in the future).
const myObj = { a:NaN, myMethod: function() { console.log("Function executed") } }
const copyMyObj = JSON.parse(JSON.stringify(myObj));
console.log(copyMyObj); // logs: { a: null }
copyMyObj.myMethod(); // logs: TypeError: copyMyObj.myMethod is not a function
const myObj = { myMethod: function() { console.log("Function executed") } }
const copyMyObj = structuredClone(myObj); // logs: DOMException: The object could not be cloned.