深入理解 JSON
我們先來看一個JS中常見的JS物件序列化成JSON字串的問題,請問,以下JS物件通過JSON.stringify
後的字串是怎樣的?先不要急著複製貼上到控制檯,先自己開啟一個程式碼編輯器或者紙,寫寫看,寫完再去仔細對比你的控制檯輸出,如果有誤記得看完全文並評論,哈哈。
var friend={ firstName: 'Good', 'lastName': 'Man', 'address': undefined, 'phone': ["1234567",undefined], 'fullName': function(){ return this.firstName + ' ' + this.lastName; } }; JSON.stringify(friend);//這一行返回什麼呢?
第二個問題,如果我想在最終JSON字串將這個’friend’的姓名全部變成大寫字母,也就是把”Good”變成”GOOD”,把”Man”變成”MAN”,那麼可以怎麼做?
基於以上兩個問題,我們再追本溯源問一下,JSON究竟是什麼東西?為什麼JSON就是易於資料交換?JSON和JS物件的區別?JS中JSON.parse
、JSON.stringify
和不常見的toJSON
,這幾個函式的引數和處理細節到底是怎樣的?
歡迎進入本次“深挖JSON之旅”,下文將從以下幾個方面去理解JSON:
- 首先是對“JSON是一種輕量的資料交換格式”的理解;
- 然後來看經常被混為一談的JSON和JS物件的區別;
- 最後我們再來看JS中這幾個JSON相關函式具體的執行細節。
希望全文能讓如之前的我一樣對JSON一知半解的親能說清楚JSON是什麼,也能熟練運用JSON,不看控制檯就知道JS物件序列化成JSON字串後輸出是啥。
一、JSON是一種格式,基於文字,優於輕量,用於交換資料
如果沒有去過JSON的官方介紹可以去一下這裡,官方介紹第一、二段已經很清楚地表述了JSON是什麼,我將JSON是什麼提煉成以下幾個方面:
1. 一種資料格式
什麼是格式?就是規範你的資料要怎麼表示,舉個例子,有個人叫“二百六”,身高“160cm”,體重“60kg”,現在你要將這個人的這些資訊傳給別人或者別的什麼東西,你有很多種選擇:
姓名“二百六”,身高“160cm”,體重“60kg”
name="二百六"&height="160cm"&weight="60kg"
<person><name>二百六</name><height>160</height><weight>60</weight></person>
{"name":"二百六","height":160,"weight":60}
- … …
以上所有選擇,傳遞的資料是一樣的,但是你可以看到形式是可以各式各樣的,這就是各種不同格式化後的資料,JSON是其中一種表示方式。
2. 基於文字的資料格式
JSON是基於文字的資料格式,相對於基於二進位制的資料,所以JSON在傳遞的時候是傳遞符合JSON這種格式(至於JSON的格式是什麼我們第二部分再說)的字串,我們常會稱為“JSON字串”。
3. 輕量級的資料格式
在JSON之前,有一個資料格式叫xml
,現在還是廣泛在用,但是JSON更加輕量,如xml
需要用到很多標籤,像上面的例子中,你可以明顯看到xml
格式的資料中標籤本身佔據了很多空間,而JSON比較輕量,即相同資料,以JSON的格式佔據的頻寬更小,這在有大量資料請求和傳遞的情況下是有明顯優勢的。
4. 被廣泛地用於資料交換
輕量已經是一個用於資料交換的優勢了,但更重要的JSON是易於閱讀、編寫和機器解析的,即這個JSON對人和機器都是友好的,而且又輕,獨立於語言(因為是基於文字的),所以JSON被廣泛用於資料交換。
以前端JS進行ajax的POST請求為例,後端PHP處理請求為例:
- 前端構造一個JS物件,用於包裝要傳遞的資料,然後將JS物件轉化為JSON字串,再傳送請求到後端;
- 後端PHP接收到這個JSON字串,將JSON字串轉化為PHP物件,然後處理請求。
可以看到,相同的資料在這裡有3種不同的表現形式,分別是前端的JS物件、傳輸的JSON字串、後端的PHP物件,JS物件和PHP物件明顯不是一個東西,但是由於大家用的都是JSON來傳遞資料,大家都能理解這種資料格式,都能把JSON這種資料格式很容易地轉化為自己能理解的資料結構,這就方便啦,在其他各種語言環境中交換資料都是如此。
二、JSON和JS物件之間的“八卦”
很多時候都聽到“JSON是JS的一個子集”這句話,而且這句話我曾經也一直這麼認為,每個符合JSON格式的字串你解析成js都是可以的,直到後來發現了一個奇奇怪怪的東西…
1. 兩個本質不同的東西為什麼那麼密切
JSON和JS物件本質上完全不是同一個東西,就像“斑馬線”和“斑馬”,“斑馬線”基於“斑馬”身上的條紋來呈現和命名,但是斑馬是活的,斑馬線是非生物。
同樣,”JSON”全名”JavaScript Object Notation”,所以它的格式(語法)是基於JS的,但它就是一種格式,而JS物件是一個例項,是存在於記憶體的一個東西。
說句玩笑話,如果JSON是基於PHP的,可能就叫PON了,形式可能就是這樣的了['propertyOne' => 'foo', 'propertyTwo' => 42,]
,如果這樣,那麼JSON可能現在是和PHP比較密切了。
此外,JSON是可以傳輸的,因為它是文字格式,但是JS物件是沒辦法傳輸的,在語法上,JSON也會更加嚴格,但是JS物件就很鬆了。
那麼兩個不同的東西為什麼那麼密切,因為JSON畢竟是從JS中演變出來的,語法相近。
2. JSON格式別JS物件語法表現上嚴格在哪
先就以“鍵值對為表現的物件”形式上,對比下兩者的不同,至於JSON還能以怎樣的形式表現,對比完後再羅列。
對比內容 | JSON | JS物件 |
---|---|---|
鍵名 | 必須是加雙引號 | 可允許不加、加單引號、加雙引號 |
屬性值 | 只能是數值(10進位制)、字串(雙引號)、布林值和null, 也可以是陣列或者符合JSON要求的物件, 不能是函式、NaN, Infinity, -Infinity和undefined |
愛啥啥 |
逗號問題 | 最後一個屬性後面不能有逗號 | 可以 |
數值 | 前導0不能用,小數點後必須有數字 | 沒限制 |
可以看到,相對於JS物件,JSON的格式更嚴格,所以大部分寫的JS物件是不符合JSON的格式的。
以下程式碼引用自這裡
var obj1 = {}; // 這只是 JS 物件 // 可把這個稱做:JSON 格式的 JavaScript 物件 var obj2 = {"width":100,"height":200,"name":"rose"}; // 可把這個稱做:JSON 格式的字串 var str1 = '{"width":100,"height":200,"name":"rose"}'; // 這個可叫 JSON 格式的陣列,是 JSON 的稍複雜一點的形式 var arr = [ {"width":100,"height":200,"name":"rose"}, {"width":100,"height":200,"name":"rose"}, {"width":100,"height":200,"name":"rose"}, ]; // 這個可叫稍複雜一點的 JSON 格式的字串 var str2='['+ '{"width":100,"height":200,"name":"rose"},'+ '{"width":100,"height":200,"name":"rose"},'+ '{"width":100,"height":200,"name":"rose"},'+ ']';
另外,除了常見的“正常的”JSON格式,要麼表現為一個物件形式{...}
,要麼表現為一個陣列形式[...]
,任何單獨的一個10進位制數值、雙引號字串、布林值和null都是有效符合JSON格式的。
3. 一個有意思的地方,JSON不是JS的子集
首先看下面的程式碼,你可以copy到控制檯執行下:
var code = '"\u2028\u2029"'; JSON.parse(code); // works fine eval(code); // fails
這兩個字元\u2028
和\u2029
分別表示行分隔符和段落分隔符,JSON.parse可以正常解析,但是當做js解析時會報錯。
三、這幾個JS中的JSON函式,弄啥嘞
在JS中我們主要會接觸到兩個和JSON相關的函式,分別用於JSON字串和JS資料結構之間的轉化,一個叫JSON.stringify
,它很聰明,聰明到你寫的不符合JSON格式的JS物件都能幫你處理成符合JSON格式的字串,所以你得知道它到底幹了什麼,免得它只是自作聰明,然後讓你Debug long time;另一個叫JSON.parse
,用於轉化json字串到JS資料結構,它很嚴格,你的JSON字串如果構造地不對,是沒辦法解析的。
而它們的引數不止一個,雖然我們經常用的時候只傳入一個引數。
此外,還有一個toJSON
函式,我們較少看到,但是它會影響JSON.stringify
。
1. 將JS資料結構轉化為JSON字串——JSON.stringify
這個函式的函式簽名是這樣的:
JSON.stringify(value[, replacer [, space]])
下面將分別展開帶1~3個引數的用法,最後是它在序列化時做的一些“聰明”的事,要特別注意。
1.1 基本使用——僅需一個引數
這個大家都會使用,傳入一個JSON格式的JS物件或者陣列,JSON.stringify({"name":"Good Man","age":18})
返回一個字串"{"name":"Good Man","age":18}"
。
可以看到本身我們傳入的這個JS物件就是符合JSON格式的,用的雙引號,也沒有JSON不接受的屬性值,那麼如果像開頭那個例子中的一樣,how to play?不急,我們先舉簡單的例子來說明這個函式的幾個引數的意義,再來說這個問題。
1.2 第二個引數可以是函式,也可以是一個陣列
- 如果第二個引數是一個函式,那麼序列化過程中的每個屬性都會被這個函式轉化和處理
- 如果第二個引數是一個陣列,那麼只有包含在這個陣列中的屬性才會被序列化到最終的JSON字串中
- 如果第二個引數是null,那作用上和空著沒啥區別,但是不想設定第二個引數,只是想設定第三個引數的時候,就可以設定第二個引數為null
這第二個引數若是函式
var friend={ "firstName": "Good", "lastName": "Man", "phone":"1234567", "age":18 }; var friendAfter=JSON.stringify(friend,function(key,value){ if(key==="phone") return "(000)"+value; else if(typeof value === "number") return value + 10; else return value; //如果你把這個else分句刪除,那麼結果會是undefined }); console.log(friendAfter); //輸出:{"firstName":"Good","lastName":"Man","phone":"(000)1234567","age":28}
如果制定了第二個引數是函式,那麼這個函式必須對每一項都有返回,這個函式接受兩個引數,一個鍵名,一個是屬性值,函式必須針對每一個原來的屬性值都要有新屬性值的返回。
那麼問題來了,如果傳入的不是鍵值對的物件形式,而是方括號的陣列形式呢?,比如上面的friend
變成這樣:friend=["Jack","Rose"]
,那麼這個逐屬性處理的函式接收到的key和value又是什麼?如果是陣列形式,那麼key是索引,而value是這個陣列項,你可以在控制檯在這個函式內部列印出來這個key和value驗證。
這第二個引數若是陣列
var friend={ "firstName": "Good", "lastName": "Man", "phone":"1234567", "age":18 }; //注意下面的陣列有一個值並不是上面物件的任何一個屬性名 var friendAfter=JSON.stringify(friend,["firstName","address","phone"]); console.log(friendAfter); //{"firstName":"Good","phone":"1234567"} //指定的“address”由於沒有在原來的物件中找到而被忽略
如果第二個引數是一個陣列,那麼只有在陣列中出現的屬性才會被序列化進結果字串,只要在這個提供的陣列中找不到的屬性就不會被包含進去,而這個陣列中存在但是源JS物件中不存在的屬性會被忽略,不會報錯。
1.3 第三個引數用於美化輸出——不建議用
指定縮排用的空白字元,可以取以下幾個值:
- 是1-10的某個數字,代表用幾個空白字元
- 是字串的話,就用該字串代替空格,最多取這個字串的前10個字元
- 沒有提供該引數 等於 設定成null 等於 設定一個小於1的數
var friend={ "firstName": "Good", "lastName": "Man", "phone":{"home":"1234567","work":"7654321"} }; //直接轉化是這樣的: //{"firstName":"Good","lastName":"Man","phone":{"home":"1234567","work":"7654321"}} var friendAfter=JSON.stringify(friend,null,4); console.log(friendAfter); /* { "firstName": "Good", "lastName": "Man", "phone": { "home": "1234567", "work": "7654321" } } */ var friendAfter=JSON.stringify(friend,null,"HAHAHAHA"); console.log(friendAfter); /* { HAHAHAHA"firstName": "Good", HAHAHAHA"lastName": "Man", HAHAHAHA"phone": { HAHAHAHAHAHAHAHA"home": "1234567", HAHAHAHAHAHAHAHA"work": "7654321" HAHAHAHA} } */ var friendAfter=JSON.stringify(friend,null,"WhatAreYouDoingNow"); console.log(friendAfter); /* 最多隻取10個字元 { WhatAreYou"firstName": "Good", WhatAreYou"lastName": "Man", WhatAreYou"phone": { WhatAreYouWhatAreYou"home": "1234567", WhatAreYouWhatAreYou"work": "7654321" WhatAreYou} } */
笑笑就好,別這樣用,序列化是為了傳輸,傳輸就是能越小越好,加莫名其妙的縮排符,解析困難(如果是字串的話),也弱化了輕量化這個特點。。
1.4 注意這個函式的“小聰明”(重要)
如果有其他不確定的情況,那麼最好的辦法就是”Have a try”,控制檯做下實驗就明瞭。
- 鍵名不是雙引號的(包括沒有引號或者是單引號),會自動變成雙引號;字串是單引號的,會自動變成雙引號
- 最後一個屬性後面有逗號的,會被自動去掉
- 非陣列物件的屬性不能保證以特定的順序出現在序列化後的字串中
這個好理解,也就是對非陣列物件在最終字串中不保證屬性順序和原來一致 - 布林值、數字、字串的包裝物件在序列化過程中會自動轉換成對應的原始值
也就是你的什麼new String("bala")
會變成"bala"
,new Number(2017)
會變成2017
- undefined、任意的函式(其實有個函式會發生神奇的事,後面會說)以及 symbol 值(symbol詳見ES6對symbol的介紹)
- 出現在非陣列物件的屬性值中:在序列化過程中會被忽略
- 出現在陣列中時:被轉換成 null
JSON.stringify({x: undefined, y: function(){return 1;}, z: Symbol("")}); //出現在非陣列物件的屬性值中被忽略:"{}" JSON.stringify([undefined, Object, Symbol("")]); //出現在陣列物件的屬性值中,變成null:"[null,null,null]"
- NaN、Infinity和-Infinity,不論在陣列還是非陣列的物件中,都被轉化為null
- 所有以 symbol 為屬性鍵的屬性都會被完全忽略掉,即便 replacer 引數中強制指定包含了它們
- 不可列舉的屬性會被忽略
2. 將JSON字串解析為JS資料結構——JSON.parse
這個函式的函式簽名是這樣的:
JSON.parse(text[, reviver])
如果第一個引數,即JSON字串不是合法的字串的話,那麼這個函式會丟擲錯誤,所以如果你在寫一個後端返回JSON字串的指令碼,最好呼叫語言本身的JSON字串相關序列化函式,而如果是自己去拼接實現的序列化字串,那麼就尤其要注意序列化後的字串是否是合法的,合法指這個JSON字串完全符合JSON要求的嚴格格式。
值得注意的是這裡有一個可選的第二個引數,這個引數必須是一個函式,這個函式作用在屬性已經被解析但是還沒返回前,將屬性處理後再返回。
var friend={ "firstName": "Good", "lastName": "Man", "phone":{"home":"1234567","work":["7654321","999000"]} }; //我們先將其序列化 var friendAfter=JSON.stringify(friend); //'{"firstName":"Good","lastName":"Man","phone":{"home":"1234567","work":["7654321","999000"]}}' //再將其解析出來,在第二個引數的函式中列印出key和value JSON.parse(friendAfter,function(k,v){ console.log(k); console.log(v); console.log("----"); }); /* firstName Good ---- lastName Man ---- home 1234567 ---- 0 7654321 ---- 1 999000 ---- work [] ---- phone Object ---- Object ---- */
仔細看一下這些輸出,可以發現這個遍歷是由內而外的,可能由內而外這個詞大家會誤解,最裡層是內部陣列裡的兩個值啊,但是輸出是從第一個屬性開始的,怎麼就是由內而外的呢?
這個由內而外指的是對於複合屬性來說的,通俗地講,遍歷的時候,從頭到尾進行遍歷,如果是簡單屬性值(數值、字串、布林值和null),那麼直接遍歷完成,如果是遇到屬性值是物件或者陣列形式的,那麼暫停,先遍歷這個子JSON,而遍歷的原則也是一樣的,等這個複合屬性遍歷完成,那麼再完成對這個屬性的遍歷返回。
本質上,這就是一個深度優先的遍歷。
有兩點需要注意:
- 如果 reviver 返回 undefined,則當前屬性會從所屬物件中刪除,如果返回了其他值,則返回的值會成為當前屬性新的屬性值。
- 你可以注意到上面例子最後一組輸出看上去沒有key,其實這個key是一個空字串,而最後的object是最後解析完成物件,因為到了最上層,已經沒有真正的屬性了。
3. 影響 JSON.stringify 的神奇函式——object.toJSON
如果你在一個JS物件上實現了toJSON
方法,那麼呼叫JSON.stringify
去序列化這個JS物件時,JSON.stringify
會把這個物件的toJSON
方法返回的值作為引數去進行序列化。
var info={ "msg":"I Love You", "toJSON":function(){ var replaceMsg=new Object(); replaceMsg["msg"]="Go Die"; return replaceMsg; } }; JSON.stringify(info); //出si了,返回的是:'"{"msg":"Go Die"}"',說好的忽略函式呢
這個函式就是這樣子的。
其實Date
型別可以直接傳給JSON.stringify
做引數,其中的道理就是,Date
型別內建了toJSON
方法。
四、小結以及關於相容性的問題
到這裡終於把,JSON和JS中的JSON,梳理了一遍,也對裡面的細節和注意點進行了一次遍歷,知道JSON是一種語法上衍生於JS語言的一種輕量級的資料交換格式,也明白了JSON相對於一般的JS資料結構(尤其是物件)的差別,更進一步,仔細地討論了JS中關於JSON處理的3個函式和細節。
不過遺憾的是,以上所用的3個函式,不相容IE7以及IE7之前的瀏覽器。有關相容性的討論,留待之後吧。如果想直接在應用上解決相容性,那麼可以套用JSON官方的js,可以解決。
如有紕漏,歡迎留言指出。
相關文章
- JSON.stringify() 的深入理解JSON
- 深入理解JMeter中的JSON ExtractorJMeterJSON
- 深入理解JavaScript系列11:根本沒有“JSON物件”這回事JavaScriptJSON物件
- 深入理解.NET Core的基元: deps.json, runtimeconfig.json, dll檔案JSON
- 理解JSON Web TokenJSONWeb
- JSON Web Token 的理解JSONWeb
- 理解JSON Web Token (一)JSONWeb
- 深入理解Isolate
- 深入理解HashMapHashMap
- 深入理解TransformORM
- 深入理解KVO
- 深入理解 JVMJVM
- 深入理解 GitGit
- 深入理解AQSAQS
- 深入理解JVMJVM
- 深入理解 TypeScriptTypeScript
- 深入理解JavaScriptCoreJavaScript
- 深入理解MVCMVC
- 深入理解 PWA
- 深入理解margin
- 深入理解ReactReact
- 深入理解BFC
- 深入理解reduxRedux
- BFC深入理解
- 深入理解 GCDGC
- 深入理解 requestAnimationFramerequestAnimationFrame
- 深入理解Eureka
- 深入理解copy
- AsyncTask深入理解
- 深入理解RunLoopOOP
- 深入理解Fsync
- 深入理解yield
- 深入淺出 JSON Web TokenJSONWeb
- JSON的一些理解JSON
- 深入理解MySQL索引MySql索引
- 深入理解ThreadLocalthread
- 深入理解JVM——物件JVM物件
- 對redis深入理解Redis