JSON:若是你願意一層一層剝開個人心,你會發現...這裏水很深——深刻理解JSON

咱們先來看一個JS中常見的JS對象序列化成JSON字符串的問題,請問,如下JS對象經過JSON.stringify後的字符串是怎樣的?先不要急着複製粘貼到控制檯,先本身打開一個代碼編輯器或者紙,寫寫看,寫完再去仔細對比你的控制檯輸出,若是有誤記得看徹底文並評論,哈哈。javascript

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",那麼能夠怎麼作?html

基於以上兩個問題,咱們再追本溯源問一下,JSON到底是什麼東西?爲何JSON就是易於數據交換?JSON和JS對象的區別?JS中JSON.parseJSON.stringify和不常見的toJSON,這幾個函數的參數和處理細節究竟是怎樣的?前端

歡迎進入本次「深挖JSON之旅」,下文將從如下幾個方面去理解JSON:java

  • 首先是對「JSON是一種輕量的數據交換格式」的理解;
  • 而後來看常常被混爲一談的JSON和JS對象的區別;
  • 最後咱們再來看JS中這幾個JSON相關函數具體的執行細節。

但願全文能讓如以前的我同樣對JSON只知其一;不知其二的親能說清楚JSON是什麼,也能熟練運用JSON,不看控制檯就知道JS對象序列化成JSON字符串後輸出是啥。ajax

1、JSON是一種格式,基於文本,優於輕量,用於交換數據

若是沒有去過JSON的官方介紹能夠去一下這裏,官方介紹第1、二段已經很清楚地表述了JSON是什麼,我將JSON是什麼提煉成如下幾個方面:json

1. 一種數據格式

什麼是格式?就是規範你的數據要怎麼表示,舉個栗子,有我的叫「二百六」,身高「160cm」,體重「60kg」,如今你要將這我的的這些信息傳給別人或者別的什麼東西,你有不少種選擇:segmentfault

  • 姓名「二百六」,身高「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處理請求爲例:

  1. 前端構造一個JS對象,用於包裝要傳遞的數據,而後將JS對象轉化爲JSON字符串,再發送請求到後端;
  2. 後端PHP接收到這個JSON字符串,將JSON字符串轉化爲PHP對象,而後處理請求。

能夠看到,相同的數據在這裏有3種不一樣的表現形式,分別是前端的JS對象、傳輸的JSON字符串、後端的PHP對象,JS對象和PHP對象明顯不是一個東西,可是因爲你們用的都是JSON來傳遞數據,你們都能理解這種數據格式,都能把JSON這種數據格式很容易地轉化爲本身能理解的數據結構,這就方便啦,在其餘各類語言環境中交換數據都是如此。

2、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,<br />也能夠是數組或者符合JSON要求的對象,<br />不能是函數、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格式的。

這裏有完整的JSON語法參考

3. 一個有意思的地方,JSON不是JS的子集

首先看下面的代碼,你能夠copy到控制檯執行下:

var code = '"\u2028\u2029"';
JSON.parse(code); // works fine
eval(code); // fails

這兩個字符\u2028\u2029分別表示行分隔符和段落分隔符,JSON.parse能夠正常解析,可是當作js解析時會報錯。

3、這幾個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驗證,記得要在函數內部返回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方法。

4、小結以及關於兼容性的問題

到這裏終於把,JSON和JS中的JSON,梳理了一遍,也對裏面的細節和注意點進行了一次遍歷,知道JSON是一種語法上衍生於JS語言的一種輕量級的數據交換格式,也明白了JSON相對於通常的JS數據結構(尤爲是對象)的差異,更進一步,仔細地討論了JS中關於JSON處理的3個函數和細節。

不過遺憾的是,以上所用的3個函數,不兼容IE7以及IE7以前的瀏覽器。有關兼容性的討論,留待以後吧。若是想直接在應用上解決兼容性,那麼能夠套用JSON官方的js,能夠解決。

若有紕漏,歡迎留言指出。

相關文章
相關標籤/搜索