从一个 JSON.parse 错误深入研究 JavaScript 的转义字符
Posted justjavac
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从一个 JSON.parse 错误深入研究 JavaScript 的转义字符相关的知识,希望对你有一定的参考价值。
JSON.parse
将一个 JSON 字符串转换为 javascript 对象。
JSON.parse('{"hello":"\world"}')
以上代码输出:
{
hello: "world"
}
是一个 JavaScript 对象,但是仔细观察会发现, "\world"
变成了 "world"
。
那么我们继续运行如下代码:
JSON.parse('{"hello":"\\world"}')
出抛出异常:
VM376:1 Uncaught SyntaxError: Unexpected token w in JSON at position 11
at JSON.parse (<anonymous>)
at <anonymous>:1:6
Unexpected token w。
好奇心不死,继续试,3 个反斜杠:
JSON.parse('{"hello":"\\\world"}')
结果是:
VM16590:1 Uncaught SyntaxError: Unexpected token w in JSON at position 11
at JSON.parse (<anonymous>)
at <anonymous>:1:6
继续,4 个反斜杠:
JSON.parse('{"hello":"\\\\world"}')
结果正常:
{
hello: "\world"
}
1 个,"world"
2 个,Error
3 个,Error
4 个,"\world"
5 个,"\world"
6 个,Error
7 个,Error
8 个,"\world"
。。。
我们换个思路,把 JSON.parse
去掉,只输出 JavaScript 字符串:
> 'hello'
"hello"
> '\hello'
"hello"
> '\\hello'
"\hello"
> '\\\hello'
"\hello"
> '\\\\hello'
"\\hello"
问题大概找到了。
把上面的规则带入到之前的 JSON.parse
代码,问题就解决了。
我们看看 JSON 的字符串解析规则:
根据这个规则,我们解析一下 "\hello"
,第 1 个字符是反斜杠( \
),所以在引号后面走最下面的分支(红线标注):
第 2 个字符是 h
,但是反斜杠后面只有 9 条路,这个不属于任何一条路,所以这个是个非法字符。
不只是 JSON,在很多语言中都会抛出类似 Error:(7,27)Illegalescape:'\h'
的错误。
但是不知道为什么 JavaScript 偏偏可以解析这个非法转义字符,而解决方式也很暴力:直接忽略。
在 es 规范我没有找到具体的章节。去看看 V8 是怎么解析的吧。
引擎读取 JavaScript 源码后首先进行词法分析,文件 /src/parsing/scanner.cc 的功能是读取源码并解析(当前最新版 6.4.286)。
找到 Scanner::Scan()
函数关键代码:
case '"':
case '\'':
token = ScanString();
break;
是一个很长的 switch
语句:如果遇到双引号( "
)、单引号( '
)则调用 ScanString()
函数。
简单解释下:以上代码是 C++ 代码,在 C++ 中单引号是字符,双引号是字符串。所以表示字符时,双引号不需要转义,但是单引号需要转义;而表示字符串时,正好相反。此处的 C++ 转义并不是我们今天要研究的转义。
在 ScanString()
函数中我们也只看重点代码:
while (c0_ != quote && c0_ != kEndOfInput && !IsLineTerminator(c0_)) {
uc32 c = c0_;
Advance();
if (c == '\\') {
if (c0_ == kEndOfInput || !ScanEscape<false, false>()) {
return Token::ILLEGAL;
}
} else {
AddLiteralChar(c);
}
}
if (c0_ != quote) return Token::ILLEGAL;
literal.Complete();
如果已经到了末尾,或者下 1 个字符是不能转义的字符,则返回 Token::ILLEGAL
。那么我们看看 ScanEscape
是不是返回了 false
呢?
template <bool capture_raw, bool in_template_literal>
bool Scanner::ScanEscape() {
uc32 c = c0_;
Advance<capture_raw>();
// Skip escaped newlines.
if (!in_template_literal && c0_ != kEndOfInput && IsLineTerminator(c)) {
// Allow escaped CR+LF newlines in multiline string literals.
if (IsCarriageReturn(c) && IsLineFeed(c0_)) Advance<capture_raw>();
return true;
}
switch (c) {
case '\'': // fall through
case '"' : // fall through
case '\\': break;
case 'b' : c = '\b'; break;
case 'f' : c = '\f'; break;
case 'n' : c = '\n'; break;
case 'r' : c = '\r'; break;
case 't' : c = '\t'; break;
case 'u' : {
c = ScanUnicodeEscape<capture_raw>();
if (c < 0) return false;
break;
}
case 'v':
c = '\v';
break;
case 'x': {
c = ScanHexNumber<capture_raw>(2);
if (c < 0) return false;
break;
}
case '0': // Fall through.
case '1': // fall through
case '2': // fall through
case '3': // fall through
case '4': // fall through
case '5': // fall through
case '6': // fall through
case '7':
c = ScanOctalEscape<capture_raw>(c, 2);
break;
}
// Other escaped characters are interpreted as their non-escaped version.
AddLiteralChar(c);
return true;
}
这个函数只有 2 处返回了 false
。
1、如果转义字符后面是 u
, u
后面不是 Unicode 字符时,返回 false
2、如果转义字符后面是 x
, x
后面不是十六进制数字时,返回 false
也就是说: '\u'
、 '\uhello'
、 '\u1'
、 '\x'
、 '\xx'
都抛出异常。
Uncaught SyntaxError: Invalid Unicode escape sequence
或
Uncaught SyntaxError: Invalid hexadecimal escape sequence
而其它非转义字符,都直接执行了后面的代码:
AddLiteralChar(c);
return true;
前面的注释也说明了这一点:
// Other escaped characters are interpreted as their non-escaped version.
其他转义字符被解释为对应的非转义版本。
综上,问题的根源就是 JavaScript 和 JSON 对转义字符的处理方式不同,导致了难以发现的 bug。JSON 遇到不能转义的字符直接抛出异常,而 JavaScript 遇到不能转义的字符直接解释为对应的非转义版本。
以上是关于从一个 JSON.parse 错误深入研究 JavaScript 的转义字符的主要内容,如果未能解决你的问题,请参考以下文章
将JSON字符串从IOS Native发送到Javascript(JSON.Parse)时出现Json Parse错误[重复]
看似有效的 JSON 上的 JSON.parse 错误 [重复]