JSON.stringify() 的深入理解

https://www.ctolib.com/topics-141914.html
https://github.com/youngwind/blog/issues/115

序言

最近在看《你所不知道的javascript》[中卷]一書稽犁,第一部分是類型和語法。本文是基于這部分的產物粹胯。在強制類型轉換->抽象值操作-> toString 部分沦偎,其中對工具函數(shù) JSON.stringify(..) 將 JSON 對象序列化為字符串部分介紹進行了詳細的介紹构订,而自己之前對 JSON.stringify(..) 認識也比較淺刊驴。

JSON.stringify() 不論是在面試還是工作中(對象的深拷貝捐寥、json 字符串序列化)都是重點福贞,總是能看到它的身影撩嚼。所以針對這個知識點記錄整理一下。

語法

參考 MDN

JSON.stringify(value[, replacer [, space]])

參數(shù)

  • value

將要序列化成 一個JSON 字符串的值挖帘。

這是第一個參數(shù)完丽,應該都不陌生,最常用的也是這個拇舀。其他兩個基本用不到逻族。

一般傳入一個對象。但是不僅僅如此你稚,還可以傳入其他值哦瓷耙。

  • replacer | 可選

可以三種類型的值:

  1. 函數(shù),在序列化過程中刁赖,被序列化的值的每個屬性都會經過該函數(shù)的轉換和處理
  2. 數(shù)組搁痛,只有包含在這個數(shù)組中的屬性名才會被序列化到最終的 JSON 字符串中
  3. null或者未提供,對象所有的屬性都會被序列化

一般情況下宇弛,我們都不傳鸡典,按第3種方式處理。

  • space | 可選

指定縮進用的空白字符串枪芒,用于美化輸出彻况。

可以指定三種類型的值:

  1. 數(shù)字,代表有多少的空格舅踪。上限為10纽甘,該值若小于1,則意味著沒有空格抽碌。
  2. 字符串悍赢,字符串的前十個字母,該字符串將被作為空格货徙。
  3. null或者未提供左权,將沒有空格。

一般情況下痴颊,我們都不傳赏迟,按第3種方式處理。

返回值

一個表示給定值的 json 字符串蠢棱。

深入理解

ToString 規(guī)則

JSON 字符串化并非嚴格意義上的強制類型轉換锌杀,但其中涉及 ToString 的相關規(guī)則:

  1. 基本類型值的字符串化規(guī)則為:null 轉換為 "null"甩栈,undefined 轉換為 "undefined",true 轉換為 "true"糕再。
  2. 數(shù)字的字符串化則遵循通用規(guī)則谤职,變成字符串數(shù)字,其中對極小和極大的數(shù)字使用指數(shù)形式:
// 1.07 連續(xù)乘以七個 1000
var a = 1.07 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000;
// 七個1000一共21位數(shù)字
a.toString(); // "1.07e21"
  1. 對普通對象來說亿鲜,除非自行定義,否則 toString()(Object.prototype.toString())返回

    內部屬性 [[Class]] 的值冤吨,如 "[object Object]"蒿柳。

如果對象有自己的 toString() 方法,字符串化時就會調用該方法并使用其返回值漩蟆。

將對象強制類型轉換為 string 是通過 ToPrimitive 抽象操作來完成的垒探。

補充:

[[Class]]:所有 typeof 返回值為 "object" 的對象(如數(shù)組)都包含一個內部屬性 [[Class]](可以把它看作一個內部的分類,而非傳統(tǒng)的面向對象意義上的類)怠李。這個屬性無法直接訪問圾叼,一般通過 Object.prototype.toString(..) 來查看。

Object.prototype.toString.call( [1,2,3] );
// "[object Array]"
Object.prototype.toString.call( /regex-literal/i );
// "[object RegExp]"

ToPrimitive:為了將值轉換為相應的基本類型值捺癞,抽象操作 ToPrimitive 會首先(通過內部操作 DefaultValue)檢查該值是否有 valueOf() 方法夷蚊。

如果有并且返回基本類型值,就使用該值進行強制類型轉換髓介。如果沒有就使用 toString()的返回值(如果存在)來進行強制類型轉換惕鼓。

如果 valueOf() 和 toString() 均不返回基本類型值,會產生 TypeError 錯誤唐础。

對大多數(shù)簡單值來說箱歧,JSON 字符串化和 toString() 的效果基本相同,只不過序列化的結果總是字符串:

JSON.stringify( 42 ); // "42"
JSON.stringify( "42" ); // ""42"" (含有雙引號的字符串)
JSON.stringify( null ); // "null"
JSON.stringify( true ); // "true"

json 序列化為字符串時一膨,需要注意的點:

  • 轉換值如果有toJSON()方法呀邢,該方法定義什么值將被序列化。
  • 非數(shù)組對象的屬性不能保證以特定的順序出現(xiàn)在序列化后的字符串中豹绪。
  • 布爾值价淌、數(shù)字、字符串的包裝對象在序列化過程中會自動轉換成對應的原始值森篷。
  • undefined输钩、任意的函數(shù)以及 symbol 值,在序列化過程中
    • 出現(xiàn)在非數(shù)組對象的屬性值中時仲智,會被忽略(包括屬性名)
    • 出現(xiàn)在數(shù)組中時买乃,會被轉換成 null(以保證單元位置不變)。
    • 函數(shù)钓辆、undefined被單獨轉換時剪验,會返回undefined肴焊,如 JSON.stringify(function(){}) or JSON.stringify(undefined).
  • 對包含循環(huán)引用的對象(對象之間相互引用,形成無限循環(huán))執(zhí)行此方法功戚,會拋出錯誤娶眷。
  • 所有以 symbol 為屬性鍵的屬性都會被完全忽略掉,即便 replacer 參數(shù)中強制指定包含了它們啸臀。
  • Date日期調用了toJSON()將其轉換為了string字符串(同Date.toISOString())届宠,因此會被當做字符串處理。
  • NaN和Infinity格式的數(shù)值及null都會被當做null乘粒。
  • 其他類型的對象豌注,包括Map/Set/weakMap/weakSet,僅會序列化可枚舉的屬性灯萍。
  • 會拋棄對象的 constructor轧铁。即 JSON.parse(JSON.stringify(obj))后得到的對象,不管這個對象原來的構造函數(shù)是什么旦棉,在深拷貝之后都會變成 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 值是指能夠呈現(xiàn)為有效 JSON 格式的值救斑。

不安全的 JSON 值:undefined、function真屯、symbol(ES6+)和包含循環(huán)引用的對象都不符合 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中創(chuàng)建一個循環(huán)引用
o.e = a;
// 循環(huán)引用在這里會產生錯誤
// JSON.stringify( a );

// 自定義的JSON序列化
a.toJSON = function() {
 // 序列化僅包含b
 return { b: this.b };
};
JSON.stringify( a ); // "{"b":42}"

toJSON() 應該“返回一個能夠被字符串化的安全的 JSON 值”凉馆,而不是“返回一個 JSON 字符串”薪寓。

  • 方式2:向 JSON.stringify(..) 傳遞一個可選參數(shù) replacer

可選參數(shù) replacer,可以是數(shù)組或者函數(shù)澜共,用來指定對象序列化過程中哪些屬性應該被處理向叉,哪些應該被排除,和 toJSON() 很像嗦董。

如果 replacer 是一個數(shù)組母谎,那么它必須是一個字符串數(shù)組,其中包含序列化要處理的對象

的屬性名稱京革,除此之外其他的屬性則被忽略奇唤。

作為函數(shù)幸斥,它有兩個參數(shù),鍵(key)值(value)都會被序列化咬扇。

  • 如果返回一個 Number, 轉換成相應的字符串被添加入JSON字符串甲葬。
  • 如果返回一個 String, 該字符串作為屬性值被添加入JSON。
  • 如果返回一個 Boolean, "true" 或者 "false"被作為屬性值被添加入JSON字符串懈贺。
  • 如果返回任何其他對象经窖,該對象遞歸地序列化成JSON字符串,對每個屬性調用replacer方法梭灿。除非該對象是一個函數(shù)钠至,這種情況將不會被序列化成JSON字符串。
  • 如果返回undefined胎源,該屬性值不會在JSON字符串中輸出。

注意: 不能用replacer方法屿脐,從數(shù)組中移除值(values)涕蚤,如若返回undefined或者一個函數(shù),將會被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;
}
// 函數(shù)
var jsonString = JSON.stringify(foo, replacer); // {"week":45,"month":7}

// 數(shù)組
JSON.stringify(foo, ['week', 'month']);  
// '{"week":45,"month":7}', 只保留“week”和“month”屬性值西疤。

JSON-js是老外寫的一個對JSON處理的小工具扰她,其中的decycle和retrocycle是專門用來破除/恢復這種循環(huán)結構的“虐基本用法如下:

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":"$"}}}"

可以看到徒役,破解循環(huán)后確實沒有報錯,但是出現(xiàn)了ref:''這樣的代碼窖壕,這種標志表示識別除了循環(huán)引用忧勿,其中ref為固定的,右邊的'...'表示它循環(huán)引用的部分瞻讽,單個$為頂層對象鸳吸。

美化序列化后的字符串

JSON.string 還有一個可選參數(shù) space,用來指定輸出的縮進格式速勇。

  • 正整數(shù)時,是指定每一級縮進的字符數(shù)晌砾,最多10個空格
  • 字符串時,是最前面的十個字符被用于每一級的縮進:
var a = {
    b: 42,
    c: "42",
    d: [1,2,3]
};

// 數(shù)字
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(..)

參考MDN

語法:

JSON.parse(text[, reviver])

參數(shù):

  • text:要被解析成JavaScript值的字符串烦磁。
  • reviver(可選):轉換器, 如果傳入該參數(shù)(函數(shù))贡羔,可以用來修改解析生成的原始值廉白,調用時機在parse函數(shù)返回之前。

返回值:Object類型, 對應給定JSON文本的對象/值

reviver 參數(shù)和 JSON.stringify 的第二個參數(shù) replacer乖寒,原理差不多猴蹂。具體為:

  • 解析值本身以及它所包含的所有屬性,會按照一定的順序(從最最里層的屬性開始楣嘁,一級級往外磅轻,最終到達頂層,也就是解析值本身)分別的去調用 reviver 函數(shù)逐虚,在調用過程中聋溜,當前屬性所屬的對象會作為 this 值,當前屬性名和屬性值會分別作為第一個和第二個參數(shù)傳入 reviver 中叭爱。
  • 如果 reviver 返回 undefined撮躁,則當前屬性會從所屬對象中刪除,如果返回了其他值买雾,則返回的值會成為當前屬性新的屬性值把曼。
  • 當遍歷到最頂層的值時,傳入 reviver 函數(shù)的參數(shù)會是空字符串 ""(因為此時已經沒有真正的屬性)和當前的解析值(有可能已經被修改過了)漓穿,當前的 this 值會是 {"": 修改過的解析值}嗤军,在編寫 reviver 函數(shù)時,要注意到這個特例晃危。
  • 函數(shù)的遍歷順序依照:從最內層開始叙赚,按照層級順序,依次向外遍歷

舉例

JSON.parse('{"p": 5}', function (k, v) {
    if(k === '') return v; // 如果到了最頂層僚饭,則直接返回屬性值震叮,
    return v * 2; // 否則將屬性值變?yōu)樵瓉淼?2 倍。
}); // { p: 10 }

JSON.parse('{"1": 1, "2": 2,"3": {"4": 4, "5": {"6": 6}}}', function (k, v) {
    console.log(k); // 輸出當前的屬性名鳍鸵,從而得知遍歷順序是從內向外的冤荆,
                    // 最后一個屬性名會是個空字符串。
    return v; // 返回原始屬性值权纤,相當于沒有傳遞 reviver 參數(shù)钓简。
}); // 1 2 4 6 5 3 ''

注意:不允許用逗號作為結尾

// both will throw a SyntaxError
JSON.parse("[1, 2, 3, 4, ]");
JSON.parse('{"foo" : 1, }');

原生 js 實現(xiàn)

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() 擁有字符串參數(shù)特性,即能動態(tài)編譯 js 代碼的能力汹想⊥獾耍可參考 神奇的eval()與new Function()

JSON.parse() 其他方式實現(xiàn):

利用 eval() 實現(xiàn),盡量避免在不必要的情況下使用古掏。 eval() '惡名昭彰'损话,擁有執(zhí)行代碼的能力(可能被惡意使用,帶來安全問題),除此之外丧枪,不能利用預編譯的優(yōu)勢進行性能優(yōu)化光涂,會比較慢。

var json = eval('(' + jsonStr + ')');

還有其他方式拧烦,比如遞歸忘闻,可參考: JSON.parse 三種實現(xiàn)方式

言盡于此,當然恋博,不止于此(你懂得)齐佳。歡迎大家來補充~

查看原文: JSON.stringify() 的深入理解

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市债沮,隨后出現(xiàn)的幾起案子炼吴,更是在濱河造成了極大的恐慌,老刑警劉巖疫衩,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件硅蹦,死亡現(xiàn)場離奇詭異,居然都是意外死亡闷煤,警方通過查閱死者的電腦和手機童芹,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來曹傀,“玉大人,你說我怎么就攤上這事饲宛〗杂洌” “怎么了?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵艇抠,是天一觀的道長幕庐。 經常有香客問我,道長家淤,這世上最難降的妖魔是什么异剥? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮絮重,結果婚禮上冤寿,老公的妹妹穿的比我還像新娘。我一直安慰自己青伤,他們只是感情好督怜,可當我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著狠角,像睡著了一般号杠。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天姨蟋,我揣著相機與錄音屉凯,去河邊找鬼。 笑死眼溶,一個胖子當著我的面吹牛悠砚,可吹牛的內容都是我干的。 我是一名探鬼主播偷仿,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼哩簿,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了酝静?” 一聲冷哼從身側響起节榜,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎别智,沒想到半個月后宗苍,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡薄榛,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年讳窟,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片敞恋。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡丽啡,死狀恐怖,靈堂內的尸體忽然破棺而出硬猫,到底是詐尸還是另有隱情补箍,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布啸蜜,位于F島的核電站坑雅,受9級特大地震影響,放射性物質發(fā)生泄漏衬横。R本人自食惡果不足惜裹粤,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蜂林。 院中可真熱鬧遥诉,春花似錦、人聲如沸噪叙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽构眯。三九已至愕难,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背猫缭。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工葱弟, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人猜丹。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓芝加,卻偏偏與公主長得像,于是被迫代替她去往敵國和親射窒。 傳聞我的和親對象是個殘疾皇子藏杖,可洞房花燭夜當晚...
    茶點故事閱讀 42,786評論 2 345