JSON.stringify() 的深入理解

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

序言

最近在看《你所不知道的javascript》[中卷]一书,第一部分是类型和语法。本文是基于这部分的产物。在强制类型转换->抽象值操作-> toString 部分,其中对工具函数 JSON.stringify(..) 将 JSON 对象序列化为字符串部分介绍进行了详细的介绍,而自己之前对 JSON.stringify(..) 认识也比较浅。

JSON.stringify() 不论是在面试还是工作中(对象的深拷贝、json 字符串序列化)都是重点,总是能看到它的身影。所以针对这个知识点记录整理一下。

语法

参考 MDN

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

参数

  • value

将要序列化成 一个JSON 字符串的值。

这是第一个参数,应该都不陌生,最常用的也是这个。其他两个基本用不到。

一般传入一个对象。但是不仅仅如此,还可以传入其他值哦。

  • replacer | 可选

可以三种类型的值:

  1. 函数,在序列化过程中,被序列化的值的每个属性都会经过该函数的转换和处理
  2. 数组,只有包含在这个数组中的属性名才会被序列化到最终的 JSON 字符串中
  3. null或者未提供,对象所有的属性都会被序列化

一般情况下,我们都不传,按第3种方式处理。

  • space | 可选

指定缩进用的空白字符串,用于美化输出。

可以指定三种类型的值:

  1. 数字,代表有多少的空格。上限为10,该值若小于1,则意味着没有空格。
  2. 字符串,字符串的前十个字母,该字符串将被作为空格。
  3. null或者未提供,将没有空格。

一般情况下,我们都不传,按第3种方式处理。

返回值

一个表示给定值的 json 字符串。

深入理解

ToString 规则

JSON 字符串化并非严格意义上的强制类型转换,但其中涉及 ToString 的相关规则:

  1. 基本类型值的字符串化规则为:null 转换为 "null",undefined 转换为 "undefined",true 转换为 "true"。
  2. 数字的字符串化则遵循通用规则,变成字符串数字,其中对极小和极大的数字使用指数形式:
// 1.07 连续乘以七个 1000
var a = 1.07 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000 * 1000;
// 七个1000一共21位数字
a.toString(); // "1.07e21"
  1. 对普通对象来说,除非自行定义,否则 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]"

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”属性值。

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(..)

参考MDN

语法:

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 三种实现方式

言尽于此,当然,不止于此(你懂得)。欢迎大家来补充~

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342