JSON.stringify
is a method often used in daily development. Can you really use it flexibly?
Before studying this article, Xiaobao wants everyone to take a few questions and learn stringify
in depth.
stringify
function has several parameters. What is the use of each parameter?stringify
serialization guidelines? null、undefined、NaN
be handled?ES6
Will there be special treatment during the serialization process of Symbol
type and BigInt
?stringify
not suitable for deep copy?stringify
?The context of the entire article is consistent with the mind map below. You can leave an impression first.
In daily programming, we often use the JSON.stringify
method to convert an object into a JSON
string form.
const stu = { name: 'zcxiaobao', age: 18 } // {"name":"zcxiaobao","age":18} console.log(JSON.stringify(stu));
But is stringify
really that simple? Let’s first take a look at the definition of stringify
in MDN
.
MDN states: The JSON.stringify()
method converts a JavaScript
object or value into JSON
string. If a replacer
function is specified, the value can be optionally replaced, or the specified replacer
is an array. Contains the properties specified by the array.
After reading the definition, Xiaobao was surprised. Does stringfy
have more than one parameter? Of course, stringify
has three parameters.
Let’s take a look at the stringify
syntax and parameter introduction:
JSON.stringify(value[, replacer [, space]])
value
: The value to be sequenced into a JSON string.replacer
(optional) If the parameter is a function , during the serialization process, each attribute of the serialized value will be converted and processed by the function;
if the parameter is an array , only the properties contained in this array will
Only the attribute names in will be serialized into the final JSON
string.
If this parameter is null
or not provided, all attributes of the object will be serialized.
space
(optional): Specifies the whitespace string used for indentation, used to beautify the output. If the parameter is a number, it represents the number of spaces. The upper limit is 10.
If the value is less than 1, it means that there are no spaces.
If the parameter is a string (when the length of the string exceeds 10 letters, the first 10 letters are taken), the string will be treated as spaces.
If the parameter is not provided (or is null), there will be no spaces
. Let’s try the use of replacer
.
replacer
as a function
replacer
as a function, it has two parameters, key ( key
) and value ( value
), and both parameters will be serialized.
At the beginning, the replacer function will be passed in an empty string as the key value, representing the object to be stringified . It is important to understand this. The replacer
function does not parse the object into key-value pairs when it comes up, but first passes in the object to be serialized . Then the properties on each object or array are passed in sequentially. If the function return value is undefined or function, the attribute value will be filtered out , and the rest will follow the return rules.
// repalcer accepts two parameters key value // key value is each key-value pair of the object // so we can simply filter based on the type of key or value function replacer(key, value) { if (typeof value === "string") { return undefined; } return value; } // function can test function by itself replacerFunc(key, value) { if (typeof value === "string") { return () => {}; } return value; } const foo = {foundation: "Mozilla", model: "box", week: 45, transport: "car", month: 7}; const jsonString = JSON.stringify(foo, replacer);
JSON
serialization result is {"week":45,"month":7}
but if the serialization is an array, if replacer
function returns undefined
or function, the current value will not Ignored and will be replaced by null
.
const list = [1, '22', 3] const jsonString = JSON.stringify(list, replacer)
The result of JSON
serialization is '[1,null,3]'
replacer
is easier to understand as an array, filtering the key values appearing in the array.
const foo = {foundation: "Mozilla", model: "box", week: 45, transport: "car", month: 7}; const jsonString = JSON.stringify(foo, ['week', 'month']);
The JSON serialization result is {"week":45,"month":7}
, and only week
and month
attribute values are retained.
appearing in non-array object attribute values: undefined
, any function, and Symbol
values will be ignored during the serialization process.
Appearing in arrays: undefined
, any function, and Symbol
values will be ignored.
converted to null
alone: undefined will be returned
// 1. The existence of these three values in the object attribute value will be ignored const obj = { name: 'zc', age: 18, // Function will be ignored sayHello() { console.log('hello world') }, // undefined will be ignored wife: undefined, // Symbol value will be ignored id: Symbol(111), // [Symbol('zc')]: 'zc', } // Output result: {"name":"zc","age":18} console.log(JSON.stringify(obj)); // 2. These three values in the array will be converted to null const list = [ 'zc', 18, // Function converted to null function sayHello() { console.log('hello world') }, // undefined converted to null undefined, // Symbol converted to null Symbol(111) ] // ["zc",18,null,null,null] console.log(JSON.stringify(list)) // 3. Separate conversion of these three values will return undefined console.log(JSON.stringify(undefined)) // undefined console.log(JSON.stringify(Symbol(111))) // undefined console.log(JSON.stringify(function sayHello() { console.log('hello world') })) // undefined
converts value. If there is toJSON()
method, whatever value toJSON()
method returns will be what value the serialization result returns, and the other values will be ignored.
const obj = { name: 'zc', toJSON(){ return 'return toJSON' } } // return toJSON console.log(JSON.stringify(obj));
Boolean values, numbers, and strings. Packaging objects for Boolean values, numbers, and strings will automatically be converted into the corresponding original value JSON during the serialization process
. stringify([new Number(1), new String("zcxiaobao"), new Boolean(true)]); // [1,"zcxiaobao",true]
Feature four is mainly aimed at special values in JavaScript
, such as NaN
, Infinity
and null in the Number
type. These three types of values will be treated as null
during serialization .
// [null,null,null,null,null] JSON.stringify([null, NaN, -NaN, Infinity, -Infinity]) // Feature 3 mentioned that the packaging objects of Boolean values, numbers, and strings will be automatically converted into the corresponding original values during the serialization process // Implicit type conversion will call the packaging class, so Number => NaN will be called first // Then convert to null // 1/0 => Infinity => null JSON.stringify([Number('123a'), +'123a', 1/0])
The toJSON
method (same as Date.toISOString()
) is deployed on the Date
object to convert it into a string, so JSON.stringify() will serialize the Date value into a time format string .
// "2022-03-06T08:24:56.138Z" JSON.stringify(new Date())
When mentioning the Symbol feature, when Symbol
type is used as a value, objects, arrays, and individual uses will be ignored, converted to null
, and converted to undefined
respectively.
Likewise, all properties with Symbol as the property key will be completely ignored, even if they are forced to be included in the replacer parameter .
const obj = { name: 'zcxiaobao', age: 18, [Symbol('lyl')]: 'unique' } function replacer(key, value) { if (typeof key === 'symbol') { return value; } } // undefined JSON.stringify(obj, replacer);
From the above case, we can see that although we forcefully specify the return Symbol
type value through replacer
, it will eventually be ignored.
JSON.stringify
stipulates: Trying to convert a value of type BigInt
will throw TypeError
const bigNumber = BigInt(1) // Uncaught TypeError: Do not know how to serialize a BigInt Console.log(JSON.stringify(bigNumber))
Feature 8 points out: Executing this method on objects containing circular references (objects refer to each other, forming an infinite loop) will throw an error.
Daily Development Deep Copy The simplest and most violent way is to use JSON.parse(JSON.stringify(obj))
, but the deep copy under this method has huge pitfalls. The key problem is that stringify
cannot handle the circular reference problem.
const obj = { name: 'zcxiaobao', age: 18, } const loopObj = { obj } // Form a circular reference obj.loopObj = loopObj; JSON.stringify(obj) /* Uncaught TypeError: Converting circular structure to JSON --> starting at object with constructor 'Object' | property 'loopObj' -> object with constructor 'Object' --- property 'obj' closes the circle at JSON.stringify (<anonymous>) at <anonymous>:10:6 */
the serialization of enumerable properties for objects (including Map/Set/WeakMap/WeakSet
), in addition to some of the situations mentioned above, stringify
also clearly stipulates that only enumerable properties will be serialized
// Non-enumerable properties will be ignored by default // {"age":18} JSON.stringify( Object.create( null, { name: { value: 'zcxiaobao', enumerable: false }, age: { value: 18, enumerable: true } } ) );
The localStorage
object is used to save the data of the entire website for a long time. The saved data has no expiration time until it is manually deleted. Usually we store it in the form of objects.
Simply call the localStorage
object method
const obj = { name: 'zcxiaobao', age: 18 } // Simply call localStorage.setItem() localStorage.setItem('zc', obj); //The final return result is [object Object] // It can be seen that simply calling localStorage fails console.log(localStorage.getItem('zc'))
localStorage
cooperates with JSON.stringify
method
localStorage.setItem('zc', JSON.stringify(obj)); //The final return result is {name: 'zcxiaobao', age: 18} Console.log(JSON.parse(localStorage.getItem('zc')))
assumes such a scenario. The backend returns a long object with many attributes, and we only need a few of them. And we need to store these attributes in localStorage
.
Option 1: Destructuring assignment + stringify
// We only need a, e, f attributes const obj = { a:1, b:2, c:3, d:4, e:5, f:6, g:7 } // Destructuring assignment const {a,e,f} = obj; // Store to localStorage localStorage.setItem('zc', JSON.stringify({a,e,f})) // {"a":1,"e":5,"f":6} console.log(localStorage.getItem('zc'))
uses stringify
's replacer
parameter
// Use replacer to filter as an array localStorage.setItem('zc', JSON.stringify(obj, ['a','e', 'f'])) // {"a":1,"e":5,"f":6} console.log(localStorage.getItem('zc'))
When replacer
is an array, we can simply filter out the attributes we need, which is a good little trick.
Using JSON.parse(JSON.stringify)
is one of the simplest and most violent ways to implement deep copies of objects. But as the title says, using this method of deep copying requires careful consideration.
Circular reference problem, stringify
will report error
function, undefined
, Symbol
will be ignored,
NaN
, Infinity
and -Infinity
will be serialized into null
...
Therefore, when using JSON.parse(JSON.stringify)
to do deep copy, you must think carefully. If there are no above-mentioned hidden dangers, JSON.parse(JSON.stringify)
is a feasible deep copy solution.
When programming with arrays, we often use the map
function. With the replacer
parameter, we can use this parameter to implement the map
function of the object.
const ObjectMap = (obj, fn) => { if (typeof fn !== "function") { throw new TypeError(`${fn} is not a function !`); } // First call JSON.stringify(obj, replacer) to implement the map function // Then call JSON.parse to re-convert it into an object return JSON.parse(JSON.stringify(obj, fn)); }; // For example, the following multiplies the attribute value of the obj object by 2 const obj = { a: 1, b: 2, c: 3 } console.log(ObjectMap(obj, (key, val) => { if (typeof value === "number") { return value * 2; } return value; }))
Many students may be wondering why there is an additional judgment required. Isn’t it possible to just return value * 2
?
As mentioned above, replacer
function first passes in the object to be serialized. The object * 2 => NaN => toJSON(NaN) => undefined => is ignored , and there will be no subsequent key-value pair analysis.
With the replacer
function, we can also delete certain attributes of the object.
const obj = { name: 'zcxiaobao', age: 18 } // {"age":18} JSON.stringify(obj, (key, val) => { // When the return value is undefined, this property will be ignored if (key === 'name') { return undefined; } return val; })
JSON.stringify
can serialize objects into strings, so we can use string methods to implement simple object equality judgments.
//Determine whether the array contains an object const names = [ {name:'zcxiaobao'}, {name:'txtx'}, {name:'mymy'}, ]; const zcxiaobao = {name:'zcxiaobao'}; // true JSON.stringify(names).includes(JSON.stringify(zcxiaobao)) // Determine whether the objects are equal const d1 = {type: 'div'} const d2 = {type: 'div'} // true JSON.stringify(d1) === JSON.stringify(d2);
With the help of the above ideas, we can also achieve simple array object deduplication.
But since the results of JSON.stringify
serialization {x:1, y:1}
and {y:1, x:1}
are different, we need to process the objects in the array before starting.
Method 1: Arrange the keys of each object in the array in dictionary order
arr.forEach(item => { const newItem = {}; Object.keys(item) // Get object key value.sort() // Key value sorting.map(key => { // Generate new object newItem[key] = item[key]; }) // Use newItem to perform deduplication operation})
But method one is a bit cumbersome. JSON.stringify
provides replacer
array format parameter, which can filter the array.
Method 2: Use replacer
array format
function unique(arr) { const keySet = new Set(); const uniqueObj = {} // Extract all keys arr.forEach(item => { Object.keys(item).forEach(key => keySet.add(key)) }) const replacer = [...keySet]; arr.forEach(item => { // All objects are filtered according to the specified key value replacer unique[JSON.stringify(item, replacer)] = item; }) return Object.keys(unique).map(u => JSON.parse(u)) } //Test unique([{}, {}, {x:1}, {x:1}, {a:1}, {x:1,a:1}, {x:1,a:1}, {x:1,a:1,b:1} ]) // Return result [{},{"x":1},{"a":1},{"x":1,"a":1},{"x":1,"a":1 ,"b":1}]