深入理解 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"
- 二百六16060
- {"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 = '"u2028u2029"';
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,可以解決。
如有紕漏,歡迎留言指出。
來自:http://web.jobbole.com/90919/