深入淺出JSON.parse的實現(xiàn)方法
前言
眾所周知,JSON.parse方法用于將一個json字符串轉換成由字符串描述的 JavaScript 值或?qū)ο?,該方法支持傳?個參數(shù),第一個參數(shù)就是需要被轉換的json字符串,第二個參數(shù)則是一個轉換器函數(shù)(reviver,也叫還原函數(shù)),這個函數(shù)會針對每個鍵/值對都調(diào)用一次,這個轉換器函數(shù)又接受2個參數(shù),第一個參數(shù)為轉換的每一個屬性名,第二個參數(shù)則為轉換的每一個屬性值,并且該函數(shù)需要返回一個值,如果返回的是undefined,則結果中就會刪除相應的鍵,如果返回了其他任何值,則該值就會成為相應鍵的值插入到結果中。
對于轉換器函數(shù)更具體點講就是:解析值本身以及它所包含的所有屬性,會按照一定的順序(從最最里層的屬性開始,一級級往外,最終到達頂層,也就是解析值本身)分別的去調(diào)用 reviver 函數(shù),在調(diào)用過程中,當前屬性所屬的對象會作為 this 值,當前屬性名和屬性值會分別作為第一個和第二個參數(shù)傳入 reviver 中。如果 reviver 返回 undefined,則當前屬性會從所屬對象中刪除,如果返回了其他值,則返回的值會成為當前屬性新的屬性值。
當遍歷到最頂層的值(解析值)時,傳入 reviver 函數(shù)的參數(shù)會是空字符串 ""(因為此時已經(jīng)沒有真正的屬性)和當前的解析值(有可能已經(jīng)被修改過了),當前的 this 值會是 {"": 修改過的解析值},在編寫 reviver 函數(shù)時,要注意到這個特例。(這個函數(shù)的遍歷順序依照:從最內(nèi)層開始,按照層級順序,依次向外遍歷)。
我們來看以下幾個示例:
const bool = JSON.parse('true'); // true
const obj = JSON.parse('{"k":1,"v":2}'); // { k:1 ,v: 2}
const obj2 = JSON.parse('{"k":1,"v":2}',(k,v) => {
if(k === 'k'){
return v + 2;
}
return v;
}); // { k:3 }
const obj3 = JSON.parse('{"k":1,"v":2}',(k,v) => {
if(k === 'k'){
return v + 2;
}
// 尤其需要注意這個特例
if(k === ""){ return v; }
return v + 1;
}); // { k:3:,v:3 }
實現(xiàn)方法
前面我們已經(jīng)熟悉了該方法的使用方式,接下來,我們就根據(jù)該方法的使用方式來實現(xiàn)這個方法。在實現(xiàn)這個方法之前,我們需要知道一點,那就是想要解析出合格的JSON數(shù)據(jù),那么數(shù)據(jù)格式就必須符合規(guī)定,例如:undefined不符合正確格式的數(shù)據(jù)格式,因此在實現(xiàn)的時候,我們都要將這種情況給考慮進去。
從前面的使用方式,我們不難看出,實際上整個解析過程就是在對整個json字符串進行遍歷,而在遍歷過程中我們就需要針對不同的數(shù)據(jù)類型做不同的處理,例如如果是解析字符串,我們只需要創(chuàng)建一個空字符串,遍歷字符串每一個字符,然后將字符拼接起來即可,當然在遍歷過程中,我們還需要對一些特殊字符或者符號進行處理。
理解了整體的思路,接下來,我們就來一步一步的實現(xiàn)這個方法吧。
創(chuàng)建一個自調(diào)用函數(shù)
我們采用的是創(chuàng)建一個自調(diào)用函數(shù),并且這個函數(shù)返回一個函數(shù),而在這個返回的函數(shù)當中,我們會提供2個參數(shù),正如前面所介紹的那樣,這2個參數(shù)分別是被解析的json字符串和轉換器函數(shù),命名為source與reviver,代碼如下所示:
const jsonParser = (() => {
// ...
return (source,reviver) => {
// ...
}
})();
這個函數(shù)內(nèi)部,我們將會定義一個變量result,用來存儲最終的解析結果,并且我們會根據(jù)第二個參數(shù)reviver是否是一個函數(shù)來確定是直接返回這個結果還是返回reviver轉換器函數(shù),這個轉換器函數(shù)也是一個自調(diào)用函數(shù),由于我們的json數(shù)據(jù)可能是嵌套的對象或者數(shù)組,因此這里我們也需要定義一個函數(shù)名,方便遞歸調(diào)用。
一些變量的定義
這里的實現(xiàn)我們最后來說,接下來我們需要先定義一些變量,比如當前字符索引值,當前字符,一些特殊字符的定義,以及需要一個變量來緩存原始json字符串,同樣的如果在解析字符串時出現(xiàn)不符合規(guī)定的字符,則需要提示錯誤,因此我們也會封裝一個error函數(shù),如下所示:
const jsonParser = (() => {
let at, // 當前字符索引值
ch, // 當前遍歷字符
text, // 緩存原始json字符串
escapee = {
'"': '"',
'\\': '\\',
'/': '/',
b: 'b',
f: '\f',
n: '\n',
r: '\r',
t: '\t'
}, // 特殊字符
error = m => {
const errorObj = {
name: 'SyntaxError',
message: m,
at,
text
};
console.error(`${JSON.stringify(errorObj)}`); // 控制臺打印錯誤
// 或者使用throw拋出錯誤,即
// throw errorObj;
},
// ...
return (source,reviver) => {
// ...
}
})();
next方法
接下來,我們還需要實現(xiàn)一個next方法,這個方法,在這個方法中,我們會依次的去讀取字符串的每一個字符,并將索引值加1,讀取字符我們可以使用String.charAt方法,該方法就是讀取字符串的每一個字符。如:
const str = "hello"; str.charAt(0); // h
需要注意的就是該方法支持傳入一個參數(shù),并且如果傳入的參數(shù),不等于我們的字符ch,則需要拋出一個兩者不相等的錯誤。
有了以上的分析,我們的next方法就很好實現(xiàn)了,如下所示:
const jsonParser = (() => {
let at,
//...
next = c => {
// 如果有傳入?yún)?shù),并且該參數(shù)值不等于當前字符,則需要給出錯誤提示
if(c && c !== ch){
error(`預期${c}代替${ch}`);
}
// 根據(jù)索引值讀取當前字符
ch = text.charAt(at);
at++; // 索引值加1
//返回當前字符
return ch;
},
//...
return (source,reviver) => {
// ...
}
})();
依據(jù)數(shù)據(jù)類型解析
接下來,我們就要根據(jù)當前解析的json字符串屬于哪一種數(shù)據(jù)類型而依次去解析了,數(shù)據(jù)類型主要分為數(shù)值number,字符串string,布爾值boolean和null,以及對象object和數(shù)組,當然還有空格字符。
也許有人好奇為什么會沒有undefined類型,我們來看如下圖所示:

如上圖所示,JSON.parse是不能夠解析undefined的,當然如果"undefined"字符串是作為一個對象的屬性值,還是可以被解析出來的,如果是undefined作為屬性值,是不會被解析出來的。如:
JSON.parse('{"a":"undefined"}'); // { a:"undefined" }
JSON.parse('{"a":undefined}'); // Unexpected token 'u', "{"a":undefined}" is not valid JSON
// 數(shù)組解析同理
布爾值,null與空白字符的解析
我們先來看最簡單的兩種數(shù)據(jù)類型的解析,由于布爾值和null兩者解析過程相似,因此歸為一類定義一個方法來解析,空白字符只需要跳過即可。代碼如下所示:
const jsonParser = (() => {
let at,
//...
white = () => {
// 注意這里為什么是使用<=而非==,因為還有類似\n這樣的空白符
while(ch && ch <= ' '){
next();
}
},
word = () => {
switch (ch) {
case 't':
next('t');
next('r');
next('u');
next('e');
return true;
case 'f':
next('f');
next('a');
next('l');
next('s');
next('e');
return false;
case 'n':
next('n');
next('u');
next('l');
next('l');
return null;
};
error(`意料之外的值:${ch}`);
},
//...
return (source,reviver) => {
// ...
}
})();
可以看到解析布爾值和null,我們只需要根據(jù)首字符是否為該類型數(shù)據(jù)的首字母即可判定解析,然后將結果返回出去即可,如果不滿足條件,則需要報錯。
數(shù)值的解析
接下來我們來看數(shù)值類型數(shù)據(jù)的解析,數(shù)值類型我們需要考慮四種情況,第一種就是正負號的解析,第二種則是e字母的解析(即科學計數(shù)法),第三種則是小數(shù)點'.'的解析,最后一種則是數(shù)字的解析。在該方法內(nèi)部,我們將創(chuàng)建2個變量,因為雖然是數(shù)值數(shù)據(jù),但是我們是一個字符一個字符的解析,而非做計算,因此就需要拼接字符串,不過拼接完之后的字符串我們需要轉換成數(shù)字,這兩個變量就做這2個工作的。
首先我們需要判斷是否為負號,從而直接拼接,然后繼續(xù)下一個字符,下一個字符我們需要將負號當做參數(shù)傳給next方法,接著我們循環(huán)當前字符是否是數(shù)字,如何判斷是否是數(shù)字呢?我們只需要比較是否大于等于0并且小于等于9即可,注意這里我們比較的是字符串的碼序,而不是單純的比數(shù)字大小來判定是否是數(shù)字。即:
const isNumber = v => v >= '0' && v <= '9';
拼接數(shù)字完成之后,我們還要繼續(xù)調(diào)用next方法進行下一步,注意這里調(diào)用不需要傳任何參數(shù)。
完成數(shù)字的拼接之后,我們接著判斷是否是小數(shù)點從而繼續(xù)拼接,小數(shù)點之后會繼續(xù)是數(shù)字,因此我們還要繼續(xù)循環(huán)數(shù)字從而繼續(xù)拼接。
最后一步就是判斷當前字符是否是e字母,注意e字母不區(qū)分大小寫,因此需要兩個判斷條件,e字母后面也有可能有正負號,因此也需要判斷是否是正負號,正負號后面還會有數(shù)字,也需要繼續(xù)拼接,從而調(diào)用next方法進行下一步。
最后把拼接后的字符串利用加號操作符轉換成數(shù)值,從而得到最終的結果,最終的結果有可能是一個NaN,因此我們還需要判斷一下是否是NaN,如果是NaN,則給出一個錯誤提示,否則直接返回最終的結果。
根據(jù)以上的分析,最終我們的number轉換方法如下所示:
const jsonParser = (() => {
let at,
//...
number = () => {
let number,string = ''; // 定義number存儲最終轉換成數(shù)字的結果,定義string變量拼接字符串
// 符號的拼接,+號通常是不會寫的,因此不需要判斷
if(ch === '-'){
string += ch;
next('-');
}
// 循環(huán)數(shù)字
while(ch >= '0' && ch <= '9'){
string += ch;
next();
}
//小數(shù)點
if(ch === '.'){
string += ch;
while(next() && ch >= '0' && ch <= '9'){
string += ch;
next();
}
}
//科學計數(shù)法
if(ch === 'e' || ch === 'E'){
string += ch;
next();
// 科學計數(shù)法e字母后還有可能是正負號
if(ch === '-' || ch === '+'){
string += ch;
next();
}
// 科學計數(shù)法e字母之后的數(shù)字
while(ch >= '0' && ch <= '9'){
string += ch;
next();
}
}
// 轉換成數(shù)值賦值給number變量
number = +string;
//判斷是否是NaN
if(isNaN(number)){
error('錯誤的數(shù)值');
}else{
return number;
}
},
//...
return (source,reviver) => {
// ...
}
})();
字符串的解析
數(shù)值類型解析完成,接下來我們來看字符串的解析,字符串的解析也是需要分情況的,首先是Unicode字符,即以u字母開頭的字符,最準確的說應該是類似這樣的unicode字符串'\u2233'的解析。遇到這樣的字符,我們會使用String.fromCharCode方法轉換成普通的字符串,這里的轉換也涉及到了一個轉換公式原理,我們會使用parseInt將其轉換成16進制的數(shù)值,然后將該數(shù)字乘以16,并相加,初始結果為0,我們會定義一個變量uChar來用作計算后的結果。
首先第一步,我們知道字符串以"開頭,因此首先我們需要判斷是否是",最開始我們也需要定義4個變量,即hex,i,uChar,string,其中hex用來存儲parseInt轉換成16進制后的結果,uChar用來存儲最終的轉換結果,i就是循環(huán)變量,string則是最終拼接出來的結果。
判斷完成之后,我們將依次循環(huán)下一個字符,在循環(huán)當中,如果遇到另一個",則代表字符串已經(jīng)拼接完成,直接返回string結果,并退出循環(huán),否則遇到當前字符是"\\",則需要將unicode字符進行轉換,首先還是調(diào)用next方法跳過該字符,然后判斷是否是u字母或者我們定義好的escapee中的特殊字符,如果兩者都不是,則需要跳出循環(huán),最后將String.fromCharCode方法轉換uChar的結果值拼接給結果變量string。
這其中額外需要注意的就是Unicode字符的計算,我們會以4為循環(huán)最終條件,去計算,并且我們在循環(huán)當中還會判斷是否是一個有限的數(shù)值,從而決定是否跳出該循環(huán)。
否則就是直接字符串拼接直到循環(huán)完成,如果不滿足相應的條件,我們最終也會給出錯誤提示。根據(jù)以上分析,最終我們拼接字符串的代碼如下所示:
const jsonParser = (() => {
let at,
//...
string = () => {
let hex,i,string,uChar;
if(ch === '"'){
// 從下一個字符開始循環(huán)
while(next()){
if(ch === '"'){
// 如果是另一個雙引號,則是字符串的結束
next();
return string;
}else if(ch === '\\'){
// 如果是Unicode字符
next();
// 如果當前字符是u字母
if(ch === 'u'){
uChar = 0;
for(i = 0;i < 4;i++){
// 轉換成16進制數(shù)
hex = parseInt(next(),16);
// 如果hex不是一個有限數(shù)值,則跳出循環(huán)
if(!isFinite(hex)){
break;
}
// 計算uChar
uChar = uChar * 16 + hex;
}
}else if(typeof escapee[ch] === 'string'){
// 如果是特殊字符,則直接拼接
string += escapee[ch];
}else{
// 跳出循環(huán)
break;
}
// 拼接最終結果
string += String.fromCharCode(uChar);
}else{
// 否則當成普通字符拼接
string += ch;
}
}
}
// 如果當前字符不是"開頭,則是一個錯誤的字符串
error('錯誤的字符串');
},
//...
return (source,reviver) => {
// ...
}
})();
數(shù)組的解析
字符串和數(shù)值以及布爾值還有null都解析完了,接下來就是數(shù)組和對象的解析了,我們先來看數(shù)組的解析。數(shù)組一定是以"["開頭的,而它里面的值有可能是字符串,或者數(shù)組或者對象等,因此在這之前我們需要先定義一個值變量value用來存儲這種不可推測的值,如下所示:
const jsonParser = (() => {
let at,
//...
value,
//...
return (source,reviver) => {
// ...
}
})();
數(shù)組的解析也不復雜,我們還是會定義一個array變量用來緩存最終的結果,接著判斷是否以[開頭,如果是就繼續(xù)下一個字符,并且有可能該字符后面有空白,因此我們需要調(diào)用white方法,緊接著我們判斷下一個字符是否是],如果是,就代表數(shù)組解析已結束,直接返回array結果。
否則循環(huán)當前字符,并將值(也就是我們定義的value變量)添加到array中,然后再調(diào)用一次white方法跳過空白字符,緊接著判斷是否是]字符,如果是就繼續(xù)下一個字符的遍歷,并返回結果,否則將逗號當做參數(shù)傳給next方法,當做下一個字符的遍歷,然后再調(diào)用一次white方法跳過空白字符。
否則最后我們就給出一個錯誤提示,錯誤的數(shù)組。根據(jù)以上的分析,最終可得代碼如下所示:
const jsonParser = (() => {
let at,
//...
value,
array = () => {
const array = [];
// [開頭則繼續(xù)下一個字符,并跳過空白
if(ch === '['){
next('[');
white();
}
// ]則解析結束,返回結果
if(ch === ']'){
next(']');
return array;
}
// 循環(huán)字符
while(ch){
array.push(value());
white();
// ]則結束解析
if(ch === ']'){
next(']');
return array;
}
// 跳過逗號字符的解析
next(',');
white();
}
// 錯誤的數(shù)組數(shù)據(jù)
error('錯誤的數(shù)組');
}
//...
return (source,reviver) => {
// ...
}
})();
對象的解析
對象的解析與數(shù)組的解析有些類似,不過對象需要考慮屬性名和屬性值,屬性名實際上就是對字符串的解析,而屬性值則與數(shù)組項一樣,是不可推測的value值,遇到:字符,我們也需要跳過,并解析下一個字符。
中間可能也會有空白字符,因此需要跳過,我們會創(chuàng)建2個變量,第一個變量用于緩存屬性名,第二個變量則是存儲結果值,我們知道對象是"{"開始,"}"結束的,除了這些需要注意的地方,其它就和解析數(shù)組一樣差不多了。
根據(jù)以上的分析,我們最終的代碼如下所示:
const jsonParser = (() => {
let at,
//...
value,
object = () => {
let key,object = {}; // 存儲屬性名和結果所定義的變量
if(ch === '{'){
// 跳過{字符解析下一個字符
next('{');
// 可能存在空白字符
white();
// 如果是}字符,則結束解析,并返回結果
if(ch === '}'){
next('}');
return object;
}
// 循環(huán)字符
while(ch){
// 屬性名即解析字符串
key = string();
// 可能存在空白字符,跳過
white();
// 跳過:字符
next(':');
// value值是一個函數(shù),下文會介紹
object[key] = value();
// 跳過空白
white();
// 如果是},則解析結束
if(ch === '}'){
next('}');
return object;
}
// 跳過,字符解析下一個字符
next(',');
// 可能存在空白字符,跳過
white();
}
}
// 如果不是以{開頭,則對象格式不符合,拋出錯誤
error('錯誤的對象');
},
//...
return (source,reviver) => {
// ...
}
})();
不可推測的值
前文也提到了不可推測的值value,它可以是數(shù)組,對象,字符串,數(shù)值,布爾值,null等其中的一個,因此該值我們定義成一個函數(shù),并根據(jù)當前字符以什么開頭來確定數(shù)據(jù)類型,從而決定使用哪個方法解析,比如是字符串,就會以"開頭,從而調(diào)用前面實現(xiàn)的string方法進行解析,如果是數(shù)組對象等同理,默認當然是以數(shù)值和布爾值以及null解析為主。
當然最開始可能也會有空白字符,需要跳過,根據(jù)以上的分析,value函數(shù)最終代碼如下所示:
const jsonParser = (() => {
let at,
//...
value,
//...定義完object方法之后再賦值value
value = () => {
// 可能存在空白字符,跳過
white();
// 判斷以什么字符開頭
switch(ch){
case '{':
return object(); // 對象解析
case '[':
return array(); // 數(shù)組解析
case '"':
return string(); // 字符串解析
case '-':
return number(); // 數(shù)值解析
default:
return ch >= '0' && ch <= '9' ? number() : word(); // 如果是數(shù)字則當做是數(shù)值解析,否則當做布爾值或null解析
}
};
return (source,reviver) => {
// ...
}
})();
返回結果:有轉換器函數(shù)與無轉換器函數(shù)
最后我們來看返回的函數(shù)的實現(xiàn)原理,首先我們創(chuàng)建了4個變量,result,text = source,at = 0,ch = ' ',分別代表最終的解析結果,原始json字符串,起始解析索引值,從0開始,起始解析字符,從空白字符開始。
接著調(diào)用value方法解析值,并賦值給結果變量result,然后調(diào)用white方法跳過空白字符,跳過空白字符之后,如果還存在字符未解析,就代表解析數(shù)據(jù)不是一個合格的json字符串,則給出錯誤提示。
最后函數(shù)結果返回2個結果,第一個結果就是如果傳入了轉換器函數(shù),則返回一個自調(diào)用函數(shù),否則返回result。如下所示:
const jsonParser = (() => {
// ...
return (source,reviver) => {
// 解析結果,原始字符串,起始解析索引值,起始解析字符
let result,text = source,at = 0,ch = ' ';
// 解析值并賦值
result = value();
// 跳過空白字符
white();
// 如果還存在解析字符,則數(shù)據(jù)不符合json規(guī)范,給出錯誤
if(ch){
error('解析語法錯誤,不是一個合格的json數(shù)據(jù)');
}
// 返回
return typeof reviver === 'function' ? (function walk(holder,key){
// ...
})({ '':result },'') : result;
}
})();
轉換器內(nèi)部的實現(xiàn)原理
還記得前面有一個這樣的示例,如下所示:
const obj3 = JSON.parse('{"k":1,"v":2}',(k,v) => {
if(k === 'k'){
return v + 2;
}
// 尤其需要注意這個特例
if(k === ""){ return v; }
return v + 1;
}); // { k:3:,v:3 }
從以上特例,我們可以得知最開始會以一個空屬性名作為遍歷的開始,這也是為什么我們的自調(diào)用函數(shù)的第一個參數(shù)值是{ '':result }的原因,第二個參數(shù)也是以空屬性名作為遍歷開始的。
在遞歸函數(shù)walk內(nèi)部,我們會定義3個變量,即循環(huán)屬性名k,緩存的屬性值v,和起始屬性值value = holder[key]。起始屬性值實際上就是原始解析結果開始,如果該值是一個對象,我們則需要遍歷該對象,如果我們的循環(huán)屬性名k是該對象的屬性,則遞歸的賦值緩存屬性值,然后判斷屬性值如果是undefined,則從對象中刪除該屬性,否則修改該屬性值,最終我們會返回調(diào)用轉換器函數(shù)的結果。
根據(jù)以上代碼分析,我們最終轉換器函數(shù)內(nèi)部實現(xiàn)原理代碼如下所示:
const jsonParser = (() => {
// ...
return (source,reviver) => {
// ...
return typeof reviver === 'function' ? (function walk(holder,key){
// 循環(huán)屬性名,緩存屬性值,讀取值
let k,v,value = holder[key];
// 如果值是對象,則需要繼續(xù)解析
if(value && typeof value === 'object'){
// 循環(huán)對象屬性值
for(k in value){
// 如果value中存在該屬性
if(Object.hasOwnProperty.call(value,k)){
// 繼續(xù)遞歸
v = walk(value,k);
// 如果屬性值不是undefined則修改屬性值,否則刪除該屬性
if(v !== undefined){
value[k] = v;
}else{
delete value[k];
}
}
}
}
// 返回轉換器函數(shù)調(diào)用的結果
return reviver.call(holder,key,value);
})({ '':result },'') : result;
}
})();
最終
將以上代碼整合起來,得到了我們的parse解析方法的實現(xiàn),以上源碼可以查看這里。
以上就是深入淺出JSON.parse的實現(xiàn)方法的詳細內(nèi)容,更多關于JSON.parse方法的資料請關注腳本之家其它相關文章!
相關文章
JavaScript進階教程之非extends的組合繼承詳解
組合繼承有時候也叫偽經(jīng)典繼承,指的是將原型鏈和借用構造函數(shù)技術組合到一塊,從而發(fā)揮二者之長的一種繼承模式,下面這篇文章主要給大家介紹了關于JavaScript進階教程之非extends的組合繼承的相關資料,需要的朋友可以參考下2022-08-08

