Extending Objects in JavaScript, Immutably

Uriel Rodriguez
The Startup
Published in
5 min readJan 11, 2021

Working with data can take two forms, either some work is done directly to it resulting in the data being fundamentally changed or it can be copied, and then have some work done to the copy. Most, if not all of the time, working with data in an immutable way is the preferred way because it increases the efficiency of being able to follow the flow of data and the changes made to it. For example, the React library uses this concept of maintaining immutability when working with state. The state of a component, or the data that describes the component at any given time, is only updated in a manner that keeps the old state intact. So, in order to begin working with data immutably, it must first be copied. For example:

let obj = { x: 1, y: 2, z: 3 };
let copy = {};
for (let key of Object.keys(obj)) {
obj[key] = copy[key];
}

In this example, we are first copying the properties from the “obj” object into the “copy” object. Of course, there are other ways to do this, and sometimes depending on the library or framework being used, there are even built-in methods that assist with this need. JavaScript provides a dedicated way to extend or copy properties from one object to another, with the Object.assign() static method. So let’s see how that method works:

let obj = { x: 1, y: 2, z: 3 };
let copy = {};
Object.assign(copy, obj);

Using the Object.assign() static method, we provided two object arguments, the first is considered the “target” object which will receive all the properties from the second argument, the “source” object. So in this case, the properties from the “obj” object are copied into the “copy” object. If the “copy” had properties that shared the same name, then those properties would be overwritten.

let target = { prop: "target prop" };
let source = { prop: "source prop", anotherProp: "anotherSourceProp" };
Object.assign(target, source);
target // => { prop: "source prop", anotherProp: "anotherSourceProp" };

Any property names shared with the source object are overwritten. The Object.assign() static method also takes additional source objects, with each overwriting the properties from the previous source object argument.

let obj = { a: 1, b: 2 };
let sourceOne = { a: 2, c: 3 };
let sourceTwo = { a: 3, b: 4 };
Object.assign(obj, sourceOne, sourceTwo);
obj // => { a: 3, b: 4, c: 3 };

Another and more modern way to extend an object is using the spread operator … which provides a short-cut way to iterating and aggregating the values of a data structure into another specified structure.

let obj = { a: 1, b: 2 };
let newObj = {...obj}; //=> { a: 1, b: 2 }
// another example
let obj = { a: 1, b: 2 };
let sourceOne = { a: 2, c: 3 };
let sourceTwo = { a: 3, b: 4 };
let newObj = {...obj, ...sourceOne, ...sourceTwo};
newObj // => { a: 3, b: 4, c: 3 }

Just like in the example using Object.assign(), using the spread operator allows the copying of properties, and overwriting of same name properties. The last object that is spread into the final object will overwrite any properties with the same name that came before it, so order matters.

So far, extending objects is relatively simple to do given the tools JavaScript provides. However, going back to the example with React, whether updating state, or extending an object, these updates, or copies are only carried out shallowly. Shallow copies are made when properties that reference primitive data types, such as numbers, strings, booleans, and so on, are copied. If a property points to a reference data type such as an object or an array, then the techniques used above to extend an object will not produce an entirely, or deeply copied object. For example:

const obj = { a: 1, b: 2, c: { d: 3 } };
const shallowCopy = {...obj};
obj.c; // => { d: 3 }
shallowCopy.c; // => { d: 3 }
obj.c = { d: 5 };
obj.c; // => { d: 5 }
shallowCopy.c; // => { d: 5 }

In the “shallowCopy” object, each property was recreated and assigned the same value as the values within the original “obj” object. But, because the object assigned to property “c” is only a reference to an object in memory, the same reference was also assigned to the new property within “shallowCopy.” Therefore, any changes made to the original “c” property, would affect the “c” property in the new “shallowCopy” object, and vice-versa. In order to deeply copy a data structure, any potential nesting of reference type properties must be accounted for.

const original = {
x:4,
y: 5,
z: 6,
obj: {
a: 23,
b: {
name: "inner",
another: {
val: 38
}
}
},
arr: [
1, 2, 3, [
9, 10
]
]
};
// define a function that will search for reference type properties and copy them returning a deep copyfunction deepCopy(obj) {
const copy = {};
const isArray = val => Array.isArray(val);
const isObject = val => typeof val === "object" && !Array.isArray(val);
function deepCopyArray(arr) {
return Array.from(arr, ele => {
if (isObject(ele)) {
return deepCopy(ele);
} else if (isArray(ele)) {
return deepCopyArray(ele);
} else return ele;
});
}
for (let [k,v] of Object.entries(obj)) {
if (isObject(v)) {
copy[k] = deepCopy(v);
} else if (isArray(v)) {
copy[k] = deepCopyArray(v);
} else {
copy[k] = v;
}
}
return copy;
}
// deeply copy original objectconst newObj = deepCopy(original);
original.obj.b.name = "original name";
original.obj; // => { a: 23, b: { name: "original name", another: {
val: 38 }}};
newObj.obj; // => { a: 23, b: { name: "inner", another: { val: 38 }}};
newObj.arr[3] = 4;
newObj.arr; // => [1, 2, 3, 4];
original.arr; // => [1, 2, 3, [9, 10]]

With this implementation, iterating over primitive data type properties on the first level are simply copied into the new object. However, when a property is identified as an array or an object, then the function recursively calls itself or a helper function, which in return recursively ensures the return of a deeply copied reference type object. After deeply copying the “original” object, changing any values to its reference type properties do not alter the new deep copy, “newObj”, and vice-versa.

Understanding that working with data immutably undertakes potentially complicated processes like copying nested properties is important. Keeping immutable data helps the debugging process by maintaining clarity with the data flow and how it changes given various processes. JavaScript provides many ways to extend and maintain immutability but further steps are needed to ensure thorough data replication. To learn more about the pros of data immutability, you can give the article below a read.

https://www.geeksforgeeks.org/why-is-immutability-so-important-in-javascript/

--

--

Uriel Rodriguez
The Startup

Flatiron School alumni and Full Stack web developer.