这是一篇遇见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""}')

而之所以报错是因为双引号的缘故。但我知道你其实想问的是,用于转义的反斜杠\哪里去了?

为什么需要转义

不妨让我们从头捋一捋,为什么需要转义。

转义的原因无非有两种(改编自百度百科

  1. 使用转义字符来表示字符集中定义的字符,如Javascript中定义了一些字母前加”"来表示常见的那些不能显示的(也就是换行,缩进,符号等)ASCII字符,如\0,\t,\n等,就称为转义字符,因为后面的字符,都不是它本来的ASCII字符意思了。
  2. 某一些特定的字符在编辑语言中被定义为特殊用途的字符。这些字符由于被定义为特殊用途,它们失去了原有的意义。例如双引号在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,而不是把对象压缩为字符串。