Implement a simple polyfill for JSON.stringify() in JavaScript.
Example
console.log(JSON.stringify([{ x: 5, y: 6 }])); // expected output: "[{"x":5,"y":6}]"
JSON.stringify() converts almost each javascript value to a string format except for a few.
We will break down this problem into two sub-problems and tackle them separately.
First, determine the typeof value and accordingly convert it to the string.
- For
function
,symbol
,undefined
return"null"
. - For
number
, if the value is finite return the value as it is else return"null"
. - For
boolean
return it as it is and forstring
return the value in double quotes. - The last thing left is
object
, there are multiple cases to handle forobject
. - If it is Date, convert it to ISO string.
- If it is a constructor of String, Boolean, or Number, convert it to those values only.
- If it is an array, convert each value of the array and return it.
- If it is a nested object recursively call the same function to stringify further.
// helper method // handle all the value types // and stringify accordingly static value(val) { switch(typeof val) { case 'boolean': case 'number': // if the value is finitie number return the number as it is // else return null return isFinite(val) ? `${val}` : `null`; case 'string': return `"${val}"`; // return null for anything else case 'function': case 'symbol': case 'undefined': return 'null'; // for object, check again to determine the objects actual type case 'object': // if the value is date, convert date to string if (val instanceof Date) { return `"${val.toISOString()}"`; } // if value is a string generated as constructor, // new String(value) else if(val.constructor === String){ return `"${val}"`; } // if value is a number or boolean generated as constructor, // new String(value), new Boolean(true) else if(val.constructor === Number || val.constructor === Boolean){ return isFinite(val) ? `${val}` : `null`; } // if value is a array, return key values as string inside [] brackets else if(Array.isArray(val)) { return `[${val.map(value => this.value(value)).join(',')}]`; } // recursively stingify nested values return this.stringify(val); } }
Second, we will handle some base cases like, if it is a null value, return 'null'
, if it is an object, get the appropriate value from the above method. At the end wrap the value inside curly braces and return them.
// main method static stringify(obj) { // if value is not an actual object, but it is undefined or an array // stringifiy it directly based on the type of value if (typeof obj !== 'object' || obj === undefined || obj instanceof Array) { return this.value(obj); } // if value is null return null else if(obj === null) { return `null`; } // remove the cycle of object // if it exists this.removeCycle(obj); // traverse the object and stringify at each level let objString = Object.keys(obj).map((k) => { return (typeof obj[k] === 'function') ? null : `"${k}": ${this.value(obj[k])}`; }); // return the stringified output return `{${objString}}`; }
The final case to handle the circular object, for that we will be using the removeCycle(obj) method to remove the cycle from the objects.
// helper method to remove cycle static removeCycle = (obj) => { //set store const set = new WeakSet([obj]); //recursively detects and deletes the object references (function iterateObj(obj) { for (let key in obj) { // if the key is not present in prototype chain if (obj.hasOwnProperty(key)) { if (typeof obj[key] === 'object'){ // if the set has object reference // then delete it if (set.has(obj[key])){ delete obj[key]; } else { //store the object reference set.add(obj[key]); //recursively iterate the next objects iterateObj(obj[key]); } } } } })(obj); }
Complete code.
class JSON { // main method static stringify(obj) { // if value is not an actual object, but it is undefined or an array // stringifiy it directly based on the type of value if (typeof obj !== 'object' || obj === undefined || obj instanceof Array) { return this.value(obj); } // if value is null return null else if(obj === null) { return `null`; } // remove the cycle from the object // if it exists this.removeCycle(obj); // traverse the object and stringify at each level let objString = Object.keys(obj).map((k) => { return (typeof obj[k] === 'function') ? null : `"${k}": ${this.value(obj[k])}`; }); // return the stringified output return `{${objString}}`; } // helper method // handle all the value types // and stringify accordingly static value(val) { switch(typeof val) { case 'boolean': case 'number': // if the value is finite number return the number as it is // else return null return isFinite(val) ? `${val}` : `null`; case 'string': return `"${val}"`; // return null for anything else case 'function': case 'symbol': case 'undefined': return 'null'; // for object, check again to determine the object's actual type case 'object': // if the value is date, convert date to string if (val instanceof Date) { return `"${val.toISOString()}"`; } // if value is a string generated as constructor, // new String(value) else if(val.constructor === String){ return `"${val}"`; } // if value is a number or boolean generated as constructor, // new String(value), new Boolean(true) else if(val.constructor === Number || val.constructor === Boolean){ return isFinite(val) ? `${val}` : `null`; } // if value is a array, return key values as string inside [] brackets else if(Array.isArray(val)) { return `[${val.map(value => this.value(value)).join(',')}]`; } // recursively stingify nested values return this.stringify(val); } } // helper method to remove cycle static removeCycle = (obj) => { //set store const set = new WeakSet([obj]); //recursively detects and deletes the object references (function iterateObj(obj) { for (let key in obj) { // if the key is not present in prototype chain if (obj.hasOwnProperty(key)) { if (typeof obj[key] === 'object'){ // if the set has object reference // then delete it if (set.has(obj[key])){ delete obj[key]; } else { //store the object reference set.add(obj[key]); //recursively iterate the next objects iterateObj(obj[key]); } } } } })(obj); } };
let obj1 = { a: 1, b: { c: 2, d: -3, e: { f: { g: -4, }, }, h: { i: 5, j: 6, }, } } let obj2 = { a: 1, b: { c: 'Hello World', d: 2, e: { f: { g: -4, }, }, h: 'Good Night Moon', }, } // cricular object const List = function(val){ this.next = null; this.val = val; }; const item1 = new List(10); const item2 = new List(20); const item3 = new List(30); item1.next = item2; item2.next = item3; item3.next = item1; console.log(JSON.stringify(item1)); console.log(JSON.stringify(obj1)); console.log(JSON.stringify(obj2)); console.log(JSON.stringify([{ x: 5, y: 6 }])); // expected output: "[{"x":5,"y":6}]" console.log(JSON.stringify([new Number(3), new String('false'), new Boolean(false), new Number(Infinity)])); // expected output: "[3,"false",false]" console.log(JSON.stringify({ x: [10, undefined, function(){}, Symbol('')]})); // expected output: "{"x":[10,null,null,null]}" console.log(JSON.stringify({a: Infinity})); Output: "{'next': {'next': {'val': 30},'val': 20},'val': 10}" "{'a': 1,'b': {'c': 2,'d': -3,'e': {'f': {'g': -4}},'h': {'i': 5,'j': 6}}}" "{'a': 1,'b': {'c': 'Hello World','d': 2,'e': {'f': {'g': -4}},'h': 'Good Night Moon'}}" "[{'x': 5,'y': 6}]" "[3,'false',false,null]" "{'x': [10,null,null,null]}" "{'a': null}"