序言
最近在看《你所不知道的javascript》[中卷]一書,第一部分是型別和語法。本文是基於這部分的產物。在強制型別轉換->抽象值操作-> toString 部分,其中對工具函式 JSON.stringify(..) 將 JSON 物件序列化為字串部分介紹進行了詳細的介紹,而自己之前對 JSON.stringify(..) 認識也比較淺。
JSON.stringify() 不論是在面試還是工作中(物件的深拷貝、json 字串序列化)都是重點,總是能看到它的身影。所以針對這個知識點記錄整理一下。
語法
參考MDN
JSON.stringify(value[, replacer [, space]])
引數
- value
將要序列化成 一個JSON 字串的值。
這是第一個引數,應該都不陌生,最常用的也是這個。其他兩個基本用不到。
一般傳入一個物件。但是不僅僅如此,還可以傳入其他值哦。
- replacer | 可選
可以三種型別的值:
- 函式,在序列化過程中,被序列化的值的每個屬性都會經過該函式的轉換和處理
- 陣列,只有包含在這個陣列中的屬性名才會被序列化到最終的 JSON 字串中
- null或者未提供,物件所有的屬性都會被序列化
一般情況下,我們都不傳,按第3種方式處理。
- space | 可選
指定縮排用的空白字串,用於美化輸出。
可以指定三種型別的值:
- 數字,代表有多少的空格。上限為10,該值若小於1,則意味著沒有空格。
- 字串,字串的前十個字母,該字串將被作為空格。
- null或者未提供,將沒有空格。
一般情況下,我們都不傳,按第3種方式處理。
返回值
一個表示給定值的 json 字串。
深入理解
ToString 規則
JSON 字串化並非嚴格意義上的強制型別轉換,但其中涉及 ToString 的相關規則:
- 基本型別值的字串化規則為:null 轉換為 "null",undefined 轉換為 "undefined",true 轉換為 "true"。
- 數字的字串化則遵循通用規則,變成字串數字,其中對極小和極大的數字使用指數形式:
// 1.07 連續乘以七個 1000
var a = 1.07 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000;
// 七個1000一共21位數字
a.toString(); // "1.07e21"
- 對普通物件來說,除非自行定義,否則 toString()(Object.prototype.toString())返回
內部屬性 [[Class]] 的值,如 "[object Object]"。
如果物件有自己的 toString() 方法,字串化時就會呼叫該方法並使用其返回值。
將物件強制型別轉換為 string 是通過 ToPrimitive 抽象操作來完成的。
補充:
[[Class]]:所有 typeof 返回值為 "object" 的物件(如陣列)都包含一個內部屬性 [[Class]](可以把它看作一個內部的分類,而非傳統的物件導向意義上的類)。這個屬性無法直接訪問,一般通過 Object.prototype.toString(..) 來檢視。
Object.prototype.toString.call( [1,2,3] );
// "[object Array]"
Object.prototype.toString.call( /regex-literal/i );
// "[object RegExp]"
上例中,陣列的內部 [[Cl
ToPrimitive:為了將值轉換為相應的基本型別值,抽象操作 ToPrimitive 會首先(通過內部操作 DefaultValue)檢查該值是否有 valueOf() 方法。
如果有並且返回基本型別值,就使用該值進行強制型別轉換。如果沒有就使用 toString()的返回值(如果存在)來進行強制型別轉換。
如果 valueOf() 和 toString() 均不返回基本型別值,會產生 TypeError 錯誤。
對大多數簡單值來說,JSON 字串化和 toString() 的效果基本相同,只不過序列化的結果總是字串:
JSON.stringify( 42 ); // "42"
JSON.stringify( "42" ); // ""42"" (含有雙引號的字串)
JSON.stringify( null ); // "null"
JSON.stringify( true ); // "true"
json 序列化為字串時,需要注意的點:
- 轉換值如果有toJSON()方法,該方法定義什麼值將被序列化。
- 非陣列物件的屬性不能保證以特定的順序出現在序列化後的字串中。
- 布林值、數字、字串的包裝物件在序列化過程中會自動轉換成對應的原始值。
- undefined、任意的函式以及 symbol 值,在序列化過程中
- 出現在非陣列物件的屬性值中時,會被忽略(包括屬性名)
- 出現在陣列中時,會被轉換成 null(以保證單元位置不變)。
- 函式、undefined被單獨轉換時,會返回undefined,如 JSON.stringify(function(){}) or JSON.stringify(undefined).
- 對包含迴圈引用的物件(物件之間相互引用,形成無限迴圈)執行此方法,會丟擲錯誤。
- 所有以 symbol 為屬性鍵的屬性都會被完全忽略掉,即便 replacer 引數中強制指定包含了它們。
- Date日期呼叫了toJSON()將其轉換為了string字串(同Date.toISOString()),因此會被當做字串處理。
- NaN和Infinity格式的數值及null都會被當做null。
- 其他型別的物件,包括Map/Set/weakMap/weakSet,僅會序列化可列舉的屬性。
- 會拋棄物件的 constructor。即 JSON.parse(JSON.stringify(obj))後得到的物件,不管這個物件原來的建構函式是什麼,在深拷貝之後都會變成 Object。
JSON.stringify({}); // '{}'
JSON.stringify(true); // 'true'
JSON.stringify("foo"); // '"foo"'
JSON.stringify([1, "false", false]); // '[1,"false",false]'
JSON.stringify({ x: 5 }); // '{"x":5}'
JSON.stringify({x: 5, y: 6});
// "{"x":5,"y":6}"
JSON.stringify([new Number(1), new String("false"), new Boolean(false)]);
// '[1,"false",false]'
JSON.stringify({x: undefined, y: Object, z: Symbol("")});
// '{}'
JSON.stringify([undefined, Object, Symbol("")]);
// '[null,null,null]'
JSON.stringify({[Symbol("foo")]: "foo"});
// '{}'
JSON.stringify({[Symbol.for("foo")]: "foo"}, [Symbol.for("foo")]);
// '{}'
JSON.stringify(
{[Symbol.for("foo")]: "foo"},
function (k, v) {
if (typeof k === "symbol"){
return "a symbol";
}
}
);
// undefined
// 不可列舉的屬性預設會被忽略:
JSON.stringify(
Object.create(
null,
{
x: { value: 'x', enumerable: false },
y: { value: 'y', enumerable: true }
}
)
);
// "{"y":"y"}"
// 序列化,然後反序列化後丟失 constructor
function Animation (name) { this.name = name; }
var dog = new Animation('小白');
console.log(dog.constructor); // ƒ Animation (name) { this.name = name; }
var obj = JSON.parse(JSON.stringify(dog));
console.log(obj.constructor); // ƒ Object() { [native code] }
安全的 json 值
所有安全的 JSON 值都可以使用 JSON.stringify(..) 字串化。安全的 JSON 值是指能夠呈現為有效 JSON 格式的值。
不安全的 JSON 值:undefined、function、symbol(ES6+)和包含迴圈引用的物件都不符合 JSON 結構標準,支援 JSON 的語言無法處理它們。例如:
JSON.stringify( undefined ); // undefined
JSON.stringify( function(){} ); // undefined
JSON.stringify(
[1,undefined,function(){},4]
); // "[1,null,null,4]"
JSON.stringify(
{ a:2, b:function(){} }
); // "{"a":2}"
將不安全的 json 轉換成安全的 json
- 方式1:toJSON
如果物件中定義了 toJSON() 方法,JSON 字串化時會首先呼叫該方法,然後用它的返回值來進行序列化。
如果要對含有非法 JSON 值的物件做字串化,或者物件中的某些值無法被序列化時,就需要定義 toJSON() 方法來返回一個安全的 JSON 值。例如:
var o = { };
var a = {
b: 42,
c: o,
d: function(){}
};
// 在a中建立一個迴圈引用
o.e = a;
// 迴圈引用在這裡會產生錯誤
// JSON.stringify( a );
// 自定義的JSON序列化
a.toJSON = function() {
// 序列化僅包含b
return { b: this.b };
};
JSON.stringify( a ); // "{"b":42}"
toJSON() 應該“返回一個能夠被字串化的安全的 JSON 值”,而不是“返回一個 JSON 字串”。
- 方式2:向 JSON.stringify(..) 傳遞一個可選引數 replacer
可選引數 replacer,可以是陣列或者函式,用來指定物件序列化過程中哪些屬性應該被處理,哪些應該被排除,和 toJSON() 很像。
如果 replacer 是一個陣列,那麼它必須是一個字串陣列,其中包含序列化要處理的物件
的屬性名稱,除此之外其他的屬性則被忽略。
作為函式,它有兩個引數,鍵(key)值(value)都會被序列化。
- 如果返回一個 Number, 轉換成相應的字串被新增入JSON字串。
- 如果返回一個 String, 該字串作為屬性值被新增入JSON。
- 如果返回一個 Boolean, "true" 或者 "false"被作為屬性值被新增入JSON字串。
- 如果返回任何其他物件,該物件遞迴地序列化成JSON字串,對每個屬性呼叫replacer方法。除非該物件是一個函式,這種情況將不會被序列化成JSON字串。
- 如果返回undefined,該屬性值不會在JSON字串中輸出。
注意: 不能用replacer方法,從陣列中移除值(values),如若返回undefined或者一個函式,將會被null取代。
所以如果要忽略某個鍵就返回 undefined,否則返回指定的值。舉例:
var a = {
b: 42,
c: "42",
d: [1,2,3]
};
JSON.stringify( a, ["b","c"] ); // "{"b":42,"c":"42"}"
JSON.stringify( a, function(k,v){
if (k !== "c") return v;
} ); // "{"b":42,"d":[1,2,3]}"
var foo = {foundation: "Mozilla", model: "box", week: 45, transport: "car", month: 7};
function replacer(key, value) {
if (typeof value === "string") {
return undefined;
}
return value;
}
// 函式
var jsonString = JSON.stringify(foo, replacer); // {"week":45,"month":7}
// 陣列
JSON.stringify(foo, ['week', 'month']);
// '{"week":45,"month":7}', 只保留“week”和“month”屬性值。
- 方式3:利用一些工具(比如JSON-js),主要處理 迴圈引用問題。可參考:
JSON-js是老外寫的一個對JSON處理的小工具,其中的decycle和retrocycle是專門用來破除/恢復這種迴圈結構的。基本用法如下:
let a={name:'aaa',link:''}
let b={name:'bbb',link:''}
a.link=b;
b.link=a;
/*decycle*/
JSON.stringify(JSON.decycle(a));
/*結果*/
"{"name":"aaa","link":{"name":"bbb","link":{"$ref":"$"}}}"
可以看到,破解迴圈後確實沒有報錯,但是出現了$ref:'$'這樣的程式碼,這種標誌表示識別除了迴圈引用,其中$ref為固定的,右邊的'$...'表示它迴圈引用的部分,單個$為頂層物件。
美化序列化後的字串
JSON.string 還有一個可選引數 space,用來指定輸出的縮排格式。
- 正整數時,是指定每一級縮排的字元數,最多10個空格
- 字串時,是最前面的十個字元被用於每一級的縮排:
var a = {
b: 42,
c: "42",
d: [1,2,3]
};
// 數字
JSON.stringify( a, null, 3 );
/*
"{
"b": 42,
"c": "42",
"d": [
1,
2,
3
]
}"
*/
// 字串
JSON.stringify( a, null, "-----" );
/*
"{
-----"b": 42,
-----"c": "42",
-----"d": [
----------1,
----------2,
----------3
-----]
}"
*/
反序列化:JSON.parse(..)
語法:
JSON.parse(text[, reviver])
引數:
- text:要被解析成JavaScript值的字串。
- reviver(可選):轉換器, 如果傳入該引數(函式),可以用來修改解析生成的原始值,呼叫時機在parse函式返回之前。
返回值:Object型別, 對應給定JSON文字的物件/值
reviver 引數和 JSON.stringify 的第二個引數 replacer,原理差不多。具體為:
- 解析值本身以及它所包含的所有屬性,會按照一定的順序(從最最裡層的屬性開始,一級級往外,最終到達頂層,也就是解析值本身)分別的去呼叫 reviver 函式,在呼叫過程中,當前屬性所屬的物件會作為 this 值,當前屬性名和屬性值會分別作為第一個和第二個引數傳入 reviver 中。
- 如果 reviver 返回 undefined,則當前屬性會從所屬物件中刪除,如果返回了其他值,則返回的值會成為當前屬性新的屬性值。
- 當遍歷到最頂層的值時,傳入 reviver 函式的引數會是空字串 ""(因為此時已經沒有真正的屬性)和當前的解析值(有可能已經被修改過了),當前的 this 值會是 {"": 修改過的解析值},在編寫 reviver 函式時,要注意到這個特例。
- 函式的遍歷順序依照:從最內層開始,按照層級順序,依次向外遍歷
舉例
JSON.parse('{"p": 5}', function (k, v) {
if(k === '') return v; // 如果到了最頂層,則直接返回屬性值,
return v * 2; // 否則將屬性值變為原來的 2 倍。
}); // { p: 10 }
JSON.parse('{"1": 1, "2": 2,"3": {"4": 4, "5": {"6": 6}}}', function (k, v) {
console.log(k); // 輸出當前的屬性名,從而得知遍歷順序是從內向外的,
// 最後一個屬性名會是個空字串。
return v; // 返回原始屬性值,相當於沒有傳遞 reviver 引數。
}); // 1 2 4 6 5 3 ''
注意:不允許用逗號作為結尾
// both will throw a SyntaxError
JSON.parse("[1, 2, 3, 4, ]");
JSON.parse('{"foo" : 1, }');
原生 js 實現
var myJson = {
parse: function (jsonStr) {
return (new Function('return ' + jsonStr))();
},
stringify: function (jsonObj) {
var result = '',
curVal;
if (jsonObj === null) {
return String(jsonObj);
}
switch (typeof jsonObj) {
case 'number':
case 'boolean':
return String(jsonObj);
case 'string':
return '"' + jsonObj + '"';
case 'undefined':
case 'function':
return undefined;
}
switch (Object.prototype.toString.call(jsonObj)) {
case '[object Array]':
result += '[';
for (var i = 0, len = jsonObj.length; i < len; i++) {
curVal = JSON.stringify(jsonObj[i]);
result += (curVal === undefined ? null : curVal) + ",";
}
if (result !== '[') {
result = result.slice(0, -1);
}
result += ']';
return result;
case '[object Date]':
return '"' + (jsonObj.toJSON ? jsonObj.toJSON() : jsonObj.toString()) + '"';
case '[object RegExp]':
return "{}";
case '[object Object]':
result += '{';
for (i in jsonObj) {
if (jsonObj.hasOwnProperty(i)) {
curVal = JSON.stringify(jsonObj[i]);
if (curVal !== undefined) {
result += '"' + i + '":' + curVal + ',';
}
}
}
if (result !== '{') {
result = result.slice(0, -1);
}
result += '}';
return result;
case '[object String]':
return '"' + jsonObj.toString() + '"';
case '[object Number]':
case '[object Boolean]':
return jsonObj.toString();
}
}
};
說明:JSON.parse() 在這裡是利用 new Function() 擁有字串引數特性,即能動態編譯 js 程式碼的能力。可參考神奇的eval()與new Function()
JSON.parse() 其他方式實現:
利用 eval() 實現,儘量避免在不必要的情況下使用。 eval() '惡名昭彰',擁有執行程式碼的能力(可能被惡意使用,帶來安全問題),除此之外,不能利用預編譯的優勢進行效能優化,會比較慢。
var json = eval('(' + jsonStr + ')');
還有其他方式,比如遞迴,可參考:JSON.parse 三種實現方式
言盡於此,當然,不止於此(你懂得)。歡迎大家來補充~