这是一篇遇见bug的反思。
问题
首先请听我描述一段业务上的需求。
有一些信息是需要客户填写,比如说录入完商品之后需要填写商品的描述信息。这些信息存储在后台,在需要时由前端请求。但出于某种原因后端只能返回字符串,而前端需要JSON,于是妥协的结果后端返回的是stringify之后的json对象的字符串。
比如我们需要的是:
{
"name": "banana"
}
而后端返回的是
'{"name": "banana"}'
然后我们再用JSON.parse
方法将字符串还原为JSON对象用于使用。
一切都很完美,直到最近遇到的这个问题出现:客户可能在商品的描述中加入英文双引号"
。于是想当然的,我们会在存储该信息的时候对双引号进行转义。例如当用户输入的是"Hello"
时,我们在存储时会存储为\"Hello\"
。
问题来了:假设后端存储的信息是{"name": "\"banana\""}
,那么它返回的字符串也是{"name": "\"banana\""}
,但是对于javascript来说返回(console打印的结果也是)的字符串却是{"name": ""banana""}
,于是调用JSON.parse
方法时报错:
JSON.parse('{"name": ""banana""}')
而之所以报错是因为双引号的缘故。但我知道你其实想问的是,用于转义的反斜杠\
哪里去了?
为什么需要转义
不妨让我们从头捋一捋,为什么需要转义。
转义的原因无非有两种(改编自百度百科)
- 使用转义字符来表示字符集中定义的字符,如Javascript中定义了一些字母前加”"来表示常见的那些不能显示的(也就是换行,缩进,符号等)ASCII字符,如\0,\t,\n等,就称为转义字符,因为后面的字符,都不是它本来的ASCII字符意思了。
- 某一些特定的字符在编辑语言中被定义为特殊用途的字符。这些字符由于被定义为特殊用途,它们失去了原有的意义。例如双引号在Javascript用于标注字符串。但如果字符串中就自带双引号。需要把自带的双引号和用于标注的双引号进行区分。于是需要对自带双引号进行转义,也就是加上反斜杠。
- 双引号用于标注字符串:
var str = "Hello"
- 如果字符串中也带了双引号,就会发生歧义:
var str = "Hello""
- 于是需要对字符串内的双引号进行转义,也就是加上反斜杠,告诉脚本引擎要区分对待:
var str = "Hello\""
请注意加上反斜杠进行转义,只是为了在书写代码时有所区分,骨子里转义双引号\"
仍然还是双引号"
。例如你可以运行下面这段代码:
var str = "\"\"\"";
console.log(str); // """
上面代码中在打印时,反斜杠已经不存在了,反斜杠存在的意义是为了保证双引号不被误解。
使用单引号也能达到同样的目的,此时也就不需要进行转义了:
var str = '"""';
console.log(str); // """
这其实能得出一个吊诡的结论:一个对象原封不动的转化为字符串后,这个字符串竟然不能还原为对象:
// 原始object对象
var obj = {
key: '\"Hello World\"'
}
var str = JSON.stringify(obj); // 转化为字符串
console.log(str)
// {"key":"\"Hello World\""}
var o = JSON.parse(str) // 还原为object时出错
解决问题
回到开始的那个问题。我们现在知道为什么当后端返回给我们{"name": "\"banana\""}
时,我们实际上得到的是{"name": ""banana""}
了。对脚本引擎来说,\"
与"
是一样的,这样的字符串当然不能被JSON.parse
。
那么什么样的字符串才能被JSON.parse
解析?后端返回字符串经过脚本引擎处理后仍然保留反斜杠的字符串:{"name": "\"banana\""}
反过来推算,如果我们想字符串能被JSON.parse
,则需要\"
中的反斜杠得以保留,也就是最终的结果应该为JSON.parse(
{“name”: “"banana"”})
,如果想反斜杠得以保留,则需要反斜杠不被用于转义,则需要在反斜杠之前再加反斜杠将其转义以防止它被用于转义(是不是有点绕,好好理解一下)。最后得出结论,后端存储时,应该存储的内容是:
{"name": "\\"banana\\""}
结论
我的忠告是,在处理类似场景时千万小心,仔细判断是否需要对反斜杠进行转义。当然上上策还是直接使用JSONP,而不是把对象压缩为字符串。