轉自:https://segmentfault.com/a/1190000021230185
通過需求學習JSON.stringify()
首先我們在開發的過程當中遇到這樣一個處理數據的需求
const todayILearn = { _id: 1, content: '今天學習 JSON.stringify(),我很開心!', created_at: 'Mon Nov 25 2019 14:03:55 GMT+0800 (中國標准時間)', updated_at: 'Mon Nov 25 2019 16:03:55 GMT+0800 (中國標准時間)' }
我們需要將上面這個對象處理成下面這個對象
const todayILearn = { id: 1, content: '今天學習 JSON.stringify(),我很開心!', createdAt: 'Mon Nov 25 2019 14:03:55 GMT+0800 (中國標准時間)', updatedAt: 'Mon Nov 25 2019 16:03:55 GMT+0800 (中國標准時間)' }
也就是在不改變屬性的值的前提下,將對象屬性修改一下。 把_id
改成 id
,把 updated_at
改成 updatedAt
,把 created_at
改成 createdAt
。我們現在通過這個小小的需求來見識一下 JSON.stringify()
的強大吧。
首先要解決這個問題我們有很多種解決方式,我們先提供兩種不優雅的解決方案:
- 方案一:一次遍歷+多聲明一個變量
// 多一個變量存儲 const todayILearnTemp = {}; for (const [key, value] of Object.entries(todayILearn)) { if (key === "_id") todayILearnTemp["id"] = value; else if (key === "created_at") todayILearnTemp["createdAt"] = value; else if (key === "updatedAt") todayILearnTemp["updatedAt"] = value; else todayILearnTemp[key] = value; } console.log(todayILearnTemp); // 結果: // { id: 1, // content: '今天學習 JSON.stringify(),我很開心!', // createdAt: 'Mon Nov 25 2019 14:03:55 GMT+0800 (中國標准時間)', // updated_at: 'Mon Nov 25 2019 16:03:55 GMT+0800 (中國標准時間)' // }
方案一完全沒有問題,可以實現。但是多聲明了一個變量又加上一層循環並且還有很多的 if
else
語句,怎么都顯得不太優雅。
- 方案二:暴力
delete
屬性和增加屬性
// 極致的暴力美學 todayILearn.id = todayILearn._id; todayILearn.createdAt = todayILearn.created_at; todayILearn.updatedAt = todayILearn.updated_at; delete todayILearn._id; delete todayILearn.created_at; delete todayILearn.updated_at; console.log(todayILearn); // 太暴力😢 //{ // content: '今天學習 JSON.stringify(),我很開心!', // id: 1, // createdAt: 'Mon Nov 25 2019 14:03:55 GMT+0800 (中國標准時間)', // updatedAt: 'Mon Nov 25 2019 16:03:55 GMT+0800 (中國標准時間)' //}
直接 delete 暴力解決太粗魯了,而且有一個缺點,屬性的順序變了。
- 方案三:序列化+
replace
美學典范
const mapObj = { _id: "id", created_at: "createdAt", updated_at: "updatedAt" }; JSON.parse( JSON.stringify(todayILearn).replace( /_id|created_at|updated_at/gi, matched => mapObj[matched]) ) // { // id: 1, // content: '今天學習 JSON.stringify(),我很開心!', // createdAt: 'Mon Nov 25 2019 14:03:55 GMT+0800 (中國標准時間)', // updatedAt: 'Mon Nov 25 2019 16:03:55 GMT+0800 (中國標准時間)' // }
接下來,系統的學習或者說是復習一遍 JSON.stringify
的基礎知識,讓我們在日常開發中更加的游刃有余。
JSON.stringify()
九大特性
JSON.stringify()
第一大特性
對於 undefined
、任意的函數以及 symbol
三個特殊的值分別作為對象屬性的值、數組元素、單獨的值時 JSON.stringify()
將返回不同的結果。
首先,我們來復習一下知識點,看一道非常簡單的面試題目:請問下面代碼會輸出什么?
const data = { a: "aaa", b: undefined, c: Symbol("dd"), fn: function() { return true; } }; JSON.stringify(data); // 輸出:? // "{"a":"aaa"}"
很簡單這道題目面試官主要考察的知識點是:
undefined
、任意的函數以及symbol
作為對象屬性值時JSON.stringify()
將跳過(忽略)對它們進行序列化
面試官追問:假設 undefined
、任意的函數以及 symbol
值作為數組元素會是怎樣呢?
JSON.stringify(["aaa", undefined, function aa() { return true }, Symbol('dd')]) // 輸出:? // "["aaa",null,null,null]"
知識點是:
undefined
、任意的函數以及symbol
作為數組元素值時,JSON.stringify()
會將它們序列化為null
我們稍微再動下腦筋,如果單獨序列化這些值會是什么樣的結果呢?
JSON.stringify(function a (){console.log('a')}) // undefined JSON.stringify(undefined) // undefined JSON.stringify(Symbol('dd')) // undefined
單獨轉換的結果就是:
undefined
、任意的函數以及symbol
被JSON.stringify()
作為單獨的值進行序列化時都會返回undefined
JSON.stringify()
第一大特性總結
undefined
、任意的函數以及symbol
作為對象屬性值時JSON.stringify()
對跳過(忽略)它們進行序列化undefined
、任意的函數以及symbol
作為數組元素值時,JSON.stringify()
將會將它們序列化為null
undefined
、任意的函數以及symbol
被JSON.stringify()
作為單獨的值進行序列化時,都會返回undefined
JSON.stringify()
第二大特性
也是在使用過程中必須要非常注意的一個點:
- 非數組對象的屬性不能保證以特定的順序出現在序列化后的字符串中。
const data = { a: "aaa", b: undefined, c: Symbol("dd"), fn: function() { return true; }, d: "ddd" }; JSON.stringify(data); // 輸出:? // "{"a":"aaa","d":"ddd"}" JSON.stringify(["aaa", undefined, function aa() { return true }, Symbol('dd'),"eee"]) // 輸出:? // "["aaa",null,null,null,"eee"]"
JSON.stringify()
第三大特性
- 轉換值如果有
toJSON()
函數,該函數返回什么值,序列化結果就是什么值,並且忽略其他屬性的值。
JSON.stringify({ say: "hello JSON.stringify", toJSON: function() { return "today i learn"; } }) // "today i learn"
JSON.stringify()
第四大特性
JSON.stringify()
將會正常序列化Date
的值。
JSON.stringify({ now: new Date() }); // "{"now":"2019-12-08T07:42:11.973Z"}"
實際上 Date
對象自己部署了 toJSON()
方法(同Date.toISOString()),因此 Date
對象會被當做字符串處理。
JSON.stringify()
第五大特性
NaN
和Infinity
格式的數值及null
都會被當做null
。
直接上代碼:
JSON.stringify(NaN) // "null" JSON.stringify(null) // "null" JSON.stringify(Infinity) // "null"
JSON.stringify()
第六大特性
關於基本類型的序列化:
- 布爾值、數字、字符串的包裝對象在序列化過程中會自動轉換成對應的原始值。
JSON.stringify([new Number(1), new String("false"), new Boolean(false)]); // "[1,"false",false]"
JSON.stringify()
第七大特性
關於對象屬性的是否可枚舉:
- 其他類型的對象,包括 Map/Set/WeakMap/WeakSet,僅會序列化可枚舉的屬性。
// 不可枚舉的屬性默認會被忽略: JSON.stringify( Object.create( null, { x: { value: 'json', enumerable: false }, y: { value: 'stringify', enumerable: true } } ) ); // "{"y","stringify"}"
JSON.stringify()
第八大特性
我們都知道實現深拷貝最簡單粗暴的方式就是序列化:JSON.parse(JSON.stringify())
,這個方式實現深拷貝會因為序列化的諸多特性從而導致諸多的坑點:比如現在我們要說的循環引用問題。
// 對包含循環引用的對象(對象之間相互引用,形成無限循環)執行此方法,會拋出錯誤。 const obj = { name: "loopObj" }; const loopObj = { obj }; // 對象之間形成循環引用,形成閉環 obj.loopObj = loopObj; function deepClone(obj) { return JSON.parse(JSON.stringify(obj)); } deepClone(obj) /** VM44:9 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 deepClone (<anonymous>:9:26) at <anonymous>:11:13 */
- 對包含循環引用的對象(對象之間相互引用,形成無限循環)執行此方法,會拋出錯誤。
這也就是為什么用序列化去實現深拷貝時,遇到循環引用的對象會拋出錯誤的原因。
JSON.stringify()
第九大特性
最后,關於 symbol
屬性還有一點要說的就是:
- 所有以
symbol
為屬性鍵的屬性都會被完全忽略掉,即便replacer
參數中強制指定包含了它們。
JSON.stringify({ [Symbol.for("json")]: "stringify" }, function(k, v) { if (typeof k === "symbol") { return v; } }) // undefined
replacer
是 JSON.stringify()
的第二個參數,我們比較少用到,所以很多時候我們會忘記 JSON.stringify()
第二個、第三個參數,場景不多,但是用的好的話會非常方便。
第二個參數和第三個參數
強大的第二個參數 replacer
replacer
參數有兩種形式,可以是一個函數或者一個數組。作為函數時,它有兩個參數,鍵(key)和值(value),函數類似就是數組方法 map
、filter
等方法的回調函數,對每一個屬性值都會執行一次該函數。如果 replacer
是一個數組,數組的值代表將被序列化成 JSON 字符串的屬性名。
1.replacer
作為函數時
可以打破九大特性的大多數特性
第二個參數replacer
非常強大, replacer
作為函數時,我們可以打破九大特性的大多數特性,我們直接來看代碼吧。
const data = { a: "aaa", b: undefined, c: Symbol("dd"), fn: function() { return true; } }; // 不用 replacer 參數時 JSON.stringify(data); // "{"a":"aaa"}" // 使用 replacer 參數作為函數時 JSON.stringify(data, (key, value) => { switch (true) { case typeof value === "undefined": return "undefined"; case typeof value === "symbol": return value.toString(); case typeof value === "function": return value.toString(); default: break; } return value; }) // "{"a":"aaa","b":"undefined","c":"Symbol(dd)","fn":"function() {\n return true;\n }"}"
雖然使用 toString() 方法有點耍流氓的意思但是不得不說第二個參數很強大。
傳入 replacer
函數的第一個參數
需要注意的是,replacer 被傳入的函數時,第一個參數不是對象的第一個鍵值對,而是空字符串作為 key 值,value 值是整個對象的鍵值對:
const data = { a: 2, b: 3, c: 4, d: 5 }; JSON.stringify(data, (key, value) => { console.log(value); return value; }) // 第一個被傳入 replacer 函數的是 {"":{a: 2, b: 3, c: 4, d: 5}} // {a: 2, b: 3, c: 4, d: 5} // 2 // 3 // 4 // 5
2.replacer
作為數組時
replacer
作為數組時,結果非常簡單,數組的值就代表了將被序列化成 JSON 字符串的屬性名。
const jsonObj = { name: "JSON.stringify", params: "obj,replacer,space" }; // 只保留 params 屬性的值 JSON.stringify(jsonObj, ["params"]); // "{"params":"obj,replacer,space"}"
有意思卻沒啥用的第三個參數 space
space
參數用來控制結果字符串里面的間距。首先看一個例子就是到這東西到底是干啥用的:
const tiedan = { name: "test", describe: "今天在學 JSON.stringify()", emotion: "like shit" }; JSON.stringify(tiedan, null, "🐷"); // 接下來是輸出結果 // "{ // 🐷"name": "test", // 🐷"describe": "今天在學 JSON.stringify()", // 🐷"emotion": "like shit" // }" JSON.stringify(tiedan, null, 2); // "{ // "name": "test", // "describe": "今天在學 JSON.stringify()", // "emotion": "like shit" // }"
上面代碼一眼就能看出第三個參數的作用了,花里胡哨的,其實這個參數還是比較雞肋的,除了好看沒啥特別的用處。我們用 \t
、 \n
等縮進能讓輸出更加格式化,更適於觀看。
- 如果是一個數字, 則在字符串化時每一級別會比上一級別縮進多這個數字值的空格(最多10個空格);
- 如果是一個字符串,則每一級別會比上一級別多縮進該字符串(或該字符串的前10個字符)。
總結
JSON.stringify()
九大特性:
一、對於 undefined
、任意的函數以及 symbol
三個特殊的值分別作為對象屬性的值、數組元素、單獨的值時的不同返回結果。
undefined
、任意的函數以及symbol
作為對象屬性值時JSON.stringify()
跳過(忽略)對它們進行序列化undefined
、任意的函數以及symbol
作為數組元素值時,JSON.stringify()
將會將它們序列化為null
undefined
、任意的函數以及symbol
被JSON.stringify()
作為單獨的值進行序列化時都會返回undefined
二、非數組對象的屬性不能保證以特定的順序出現在序列化后的字符串中。
三、轉換值如果有 toJSON()
函數,該函數返回什么值,序列化結果就是什么值,並且忽略其他屬性的值。
四、JSON.stringify()
將會正常序列化 Date
的值。
五、NaN
和 Infinity
格式的數值及 null
都會被當做 null
。
六、布爾值、數字、字符串的包裝對象在序列化過程中會自動轉換成對應的原始值。
七、其他類型的對象,包括 Map/Set/WeakMap/WeakSet,僅會序列化可枚舉的屬性。
八、對包含循環引用的對象(對象之間相互引用,形成無限循環)執行此方法,會拋出錯誤。
九、所有以 symbol
為屬性鍵的屬性都會被完全忽略掉,即便 replacer
參數中強制指定包含了它們。
JSON.stringify()
第二個參數和第三個參數
強大的第二個參數:
- 作為函數時,它有兩個參數,鍵(key)和值(value),函數類似就是數組方法
map
、filter
等方法的回調函數,對每一個屬性值都會執行一次該函數(期間我們還簡單實現過一個map
函數)。 - 如果
replacer
是一個數組,數組的值代表將被序列化成 JSON 字符串的屬性名。
華麗的第三個參數:
- 如果是一個數字, 則在字符串化時每一級別會比上一級別縮進多這個數字值的空格(最多10個空格);
- 如果是一個字符串,則每一級別會比上一級別多縮進該字符串(或該字符串的前10個字符)。
注意:
第一個例子的方案三,有小伙伴提示說這個方案會有風險,確實是這樣的(可能會把對象的值給替換掉)。大家慎用吧,大部分情況下這樣使用是 ok 的。小伙伴們提供的第四種方案還是很不錯的:
const todayILearn = { _id: 1, content: '今天學習 JSON.stringify(),我很開心!', created_at: 'Mon Nov 25 2019 14:03:55 GMT+0800 (中國標准時間)', updated_at: 'Mon Nov 25 2019 16:03:55 GMT+0800 (中國標准時間)' } const mapObj = { _id: "id", created_at: "createdAt", updated_at: "updatedAt" }; Object.fromEntries(Object.entries(todayILearn).map(([k, v]) => [mapObj[k]||k, v])) // { // id: 1, // content: '今天學習 JSON.stringify(),我很開心!', // createdAt: 'Mon Nov 25 2019 14:03:55 GMT+0800 (中國標准時間)', // updatedAt: 'Mon Nov 25 2019 16:03:55 GMT+0800 (中國標准時間)' // }