If you have read my previous blog post regarding how to flatten a javascript array, you are already familiar with the step by step approach we used to come up with the final solution. That’s what we will be doing in this tutorial as well. We will start with a simple implementation first and along the way we will end up using:
- Object.assign
- recursion
- concat
- spread operator
- map
Here’s the final implementation.
const isPlainObject = function (obj) { return Object.prototype.toString.call(obj) === '[object Object]'; }; const flatten = (obj) => { return Object.assign( {}, ...function _flatten(obj) { return [].concat(...Object.keys(obj) .map(k => { if (isPlainObject(obj[k])) { return _flatten(obj[k]); } else { return { [k]: obj[k] } } }) ) }(obj) ) }
Testing
You can find the different implementations of flattening a nested object on Codepen. Click on the Console button on the left bottom bar to see the expected output.
Initial implementation
Let’s start simple and see how we can flatten objects only one level deep. We loop through the object keys and perform a check on each key.
If its value is not an object, we add the key into our new flatObj. If it is an object, we loop through the keys of this object and add the key/values into the flat object. Simple, right?
const flattenObject = obj => { let flatObj = {}; Object.keys(obj).forEach(key => { if (isPlainObject(obj[key])) { Object.keys(obj[key]).forEach(otherKey => { flatObj[otherKey] = obj[key][otherKey]; }); } else { flatObj[key] = obj[key]; } }); return flatObj; }
Recursive implementation
Problem with above solution is that it only works for objects one level deep. We will use recursion to be able to solve the problem with any level of depth.
let flattenObject = obj => { let flatObj = {}; Object.keys(obj).forEach(key => { if (isPlainObject(obj[key])) { Object.assign(flatObj, flattenObject(obj[key])); } else { flatObj[key] = obj[key]; } }); return flatObj; }
For this implementation apart from recursion we also used Object.assign()
, so let’s briefly review what Object.assign does.
Object.assign
Object.assign() is used to copy the properties and values of one or more source objects to a target object.
Object.assign(target, ...sources)
It can be used to:
- clone an object
- merge objects
Final implementation
Let’s break down what happens in the final implementation.
We use and return Object.assign to merge the nested objects into the empty target object{}
.
For a minute let’s ignore the spread operator ...
on line 4 and let’s focus on the _flatten
recursive function. This function is almost identical with the _flatten function we defined in the ‘Using concat, map & spread operator’ section of how to flatten an Array The only difference here is that we loop through Object.keys(obj) and we check if the value of each key is an object.
Feel free to check above blog post for further explanation. In a nutshell, we combine concat function with spread operator [].concat(...arr)
to get back a flat array one level deep. In order to accommodate indefinitely nested objects, we use map and recursively call _flatten in case the value is an object.
In the end _flatten method will return an array of flat objects. With the use of spread operator on line 4 we spread out these objects and merge them with Object.assign in a single object.
const flatten = (obj) => { return Object.assign( {}, ...function _flatten(obj) { return [].concat(...Object.keys(obj) .map(k => { if (isPlainObject(obj[k])) { return _flatten(obj[k]); } else { return { [k]: obj[k] } } }) ) }(obj) ) }
Including path
const flattenPath = (obj) => { return Object.assign( {}, ...function _flatten(obj, path = []) { return [].concat(...Object.keys(obj) .map(k => { if (isPlainObject(obj[k])) { return _flatten(obj[k], path.concat([k])); } else { return { [path.concat([k]).join('.')]: obj[k] } } }) ) }(obj) ) }
Testing above function with {ab: {cd: {e:'foo', f:'bar'}, g:'foo2'}}
would give back:
flattenPath({ab: {cd: {e:'foo', f:'bar'}, g:'foo2'}}) { 'ab.cd.e' : 'foo', 'ab.cd.f' : 'bar', 'ab.g' : 'foo2' }