Flattening an object means converting an object to a single depth object. It is a great way for visualisation of an object. It is not converting the object to a string, but a resulting object only.

If you convert object to a string, this is what you get.

let a = {
	name : "John Doe",
	birth: {
        month: 12,
        year: 1990
    }
}

JSON.stringify(a) 
// This gives {"name":"John Doe","birth":{"month":12,"year":1990}}

What we want is something like this

{
	name : "John Doe",
    birth.month : 12,
    birth.year : 1990
}

Much cleaner and mainly iterable. This is great if you want to log some object and want to make it readable but don't want to create multi depth object. So, because we convert any depth object to a single depth object, it is called flattening.

Now how to write your own flatten function. Below is the complete flatten function.

const flatten = (obj) => {
	let tempObj = {};
    
    const flattenHelper = (ob,k='') => {
    	let tKey = k.length ? k + '.' : '';
        
        Object.keys(ob).forEach((key) => {
        	if (typeof ob[key] == "object") {
            	flattenHelper(ob, tKey + key);
            } else {
            	tempObj[tKey + key] = ob[key];
            }
        });
    }
    
    flattenHelper(obj);
    
    return tempObj;
}

If you are interested in what each line does, you can continue reading.

let tempObj = {}

We have defined an empty object that we are going to populate and then return it in the end.

const flattenHelper = (ob,k='') => { ....

Now we have added a helper function inside our main function, we can also create an external function and call it inside, but let's make use of closures in javascript for this one. There are also ways where you can do with just one main function, but I personally feel this to be more intuitive.

Notice we are using two parameters, one is the object itself and another is a prefix key. Since there is no known depth of the original object beforehand, we have to use some kind of recursion. The second parameter has a default value empty string, but we will pass current calculated keys when we recursively call the helper function. This will make more sense later on.

let tKey = k.length ? k + '.' : '';

This is a temporary key that adds the dot after each depth, you can replace the dot with - , _ or anything to separate each depth key. If you want birth_year instead of  birth.year , you can just replace it. Also we don't want the key to start with a dot, so we place a check if the passed key is empty, without this check you will get keys like .birth.year.

Object.keys(ob).forEach((key) => {....

We are iterating over each key.

if (typeof ob[key] == "object") {
            	flattenHelper(ob, tKey + key);
            } else {
            	tempObj[tKey + key] = ob[key];
            }

Now we check if the current key's value is an object or not, if not we know this is the end of the level for the current key, so we assign the value to the current (prefix+key) . If it's an object then we will again call the helper function with the updates prefix key value.

flattenHelper(obj);

Then we just apply the function on obj, and our tempObjj is now ready to be returned.

Please note this is not the only way to do this, there are multiple other ways as well, so feel free to make your own function!