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,undefinedreturn"null". - For
number, if the value is finite return the value as it is else return"null". - For
booleanreturn it as it is and forstringreturn 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}"