先记住这个答案
JSON.stringify 只输出 JSON 能表示的值,undefined、函数和 Symbol 在 JSON 里都没有对应的类型,处理方式取决于它们的位置。在对象里,属性连同键名一起被删掉,因为 JSON 对象里少一个成员不会破坏结构。在数组里,这些元素会被写成 null,因为删掉会让后面元素的下标全部前移。如果直接传这三类值,JSON.stringify 没有东西可输出,返回 undefined,注意不是字符串 "undefined"。
- 对象里:属性连键一起被删掉
- 数组里:变成
null,保留下标 - 顶层:返回
undefined,不是字符串 - Symbol 作为键时,这个属性也会被忽略
同一组值放在三个位置
把同样的三个值分别放进对象、数组,以及直接传给 JSON.stringify。
const fn = () => 1
const token = Symbol('token')
console.log(JSON.stringify({ a: undefined, b: fn, c: token, d: 0 }))
console.log(JSON.stringify([undefined, fn, token, 0]))
console.log(JSON.stringify(undefined))
console.log(typeof JSON.stringify(fn)) // 顶层返回的不是字符串查看输出与解释
{"d":0}
[null,null,null,0]
undefined
undefined第一行只剩下能序列化的 d,其他三个属性连键名都没有了。第二行数组长度还是 4,前三个位置用 null 占着,这样 0 仍然在下标 3。第三、四行说明顶层传入这些值时返回的是 undefined,拿它当 fetch 的 body,等于没有请求体,后端拿到的是空内容;要是再和别的字符串拼接,又会变成 "undefined" 这几个字母。
为什么对象和数组处理不同?对象的成员是按名字访问的,少一个不影响别的。数组靠顺序表达数据,删掉第一个元素,原来的第二个就变成了第一个,数据就错位了,所以只能用 null 占位。
项目里真正会踩的坑
面试时说出上面的规则只是第一步。更重要的是知道它在业务里会造成什么问题。
const patch = { nickname: undefined, avatar: null }
console.log(JSON.stringify(patch)) // nickname 被删掉了
// 需要保留字段时,用 replacer 把 undefined 转成 null
console.log(JSON.stringify(patch, (key, value) => (value === undefined ? null : value)))
const id = Symbol('id')
console.log(JSON.stringify({ [id]: 1, a: 1 })) // Symbol 作为键也会被忽略
console.log(JSON.stringify([, 1])) // 稀疏数组的空位也变成 null查看输出与解释
{"avatar":null}
{"nickname":null,"avatar":null}
{"a":1}
[null,1]很多 PATCH 接口的约定是:字段不传表示不修改,传 null 表示清空。表单里用户清空了昵称,前端状态里是 undefined,序列化后字段直接消失,后端以为不用改,昵称就没被清掉。反过来,如果后端把 null 当成清空,而你用 replacer 把所有 undefined 都转成 null,又可能清掉了本来不想动的字段。所以提交前要按接口约定,明确每个字段是「不传」还是「传 null」。
Symbol 作为属性键时,不管值是什么,这个属性都不会被序列化,因为 JSON 的键只能是字符串。稀疏数组里的空位和显式写 undefined 的元素,输出都是 null。
- 别用 stringify 结果判断空对象
JSON.stringify(obj) === '{}'看起来能判断空对象,但{ a: undefined }和{ [Symbol()]: 1 }也会得到'{}',Map、Set 和正则对象同样如此。判断空对象用Object.keys(obj).length === 0,需要把 Symbol 键也算上就用Reflect.ownKeys。
容易答错的地方
- 以为所有位置都会变成
null - 只有数组元素会变成
null。对象里是整个属性消失,顶层是返回undefined。三种情况要分开记。 - 以为
JSON.stringify(undefined)返回'undefined' - 它返回的是 JavaScript 的
undefined,对它取typeof结果是'undefined',但它不是字符串。字符串"undefined"是另一个东西,只在你把这个返回值再拼接或者强转时才会出现。
面试官还会怎么问?
函数和 Symbol 在对象里被删掉,Date 和 Map 呢?
Date 有 toJSON() 方法,会被转成 ISO 字符串。Map 和 Set 没有可枚举的自有属性,会被序列化成 {},内容全丢但属性还在,比 undefined 更隐蔽。RegExp 也会变成 {}。
对象的 toJSON() 返回 undefined 会怎样?
和值本身是 undefined 一样处理。这个对象作为属性时,属性会被删掉;作为数组元素时变成 null;作为顶层值时 JSON.stringify 返回 undefined。
怎么让函数也能被序列化?
JSON 设计上就不传代码,一般也不该传。确实有需要,可以在 replacer 里返回 value.toString() 得到源码字符串,但接收端要用 eval 或 new Function 才能还原,这有严重的安全风险。正确的做法是传数据,让接收端根据数据选择对应的逻辑。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。