从一个 JSON.parse 错误深入研究 JavaScript 的转义字符

Posted justjavac

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了从一个 JSON.parse 错误深入研究 JavaScript 的转义字符相关的知识,希望对你有一定的参考价值。

JSON.parse 将一个 JSON 字符串转换为 javascript 对象。

 
   
   
 
  1. JSON.parse('{"hello":"\world"}')

以上代码输出:

 
   
   
 
  1. {

  2.  hello: "world"

  3. }

是一个 JavaScript 对象,但是仔细观察会发现, "\world" 变成了 "world"

那么我们继续运行如下代码:

 
   
   
 
  1. JSON.parse('{"hello":"\\world"}')

出抛出异常:

 
   
   
 
  1. VM376:1 Uncaught SyntaxError: Unexpected token w in JSON at position 11

  2.    at JSON.parse (<anonymous>)

  3.    at <anonymous>:1:6

  4. Unexpected token w

好奇心不死,继续试,3 个反斜杠:

 
   
   
 
  1. JSON.parse('{"hello":"\\\world"}')

结果是:

 
   
   
 
  1. VM16590:1 Uncaught SyntaxError: Unexpected token w in JSON at position 11

  2.    at JSON.parse (<anonymous>)

  3.    at <anonymous>:1:6

继续,4 个反斜杠:

 
   
   
 
  1. JSON.parse('{"hello":"\\\\world"}')

结果正常:

 
   
   
 
  1. {

  2. hello: "\world"

  3. }

  • 1 个,"world"

  • 2 个,Error

  • 3 个,Error

  • 4 个,"\world"

  • 5 个,"\world"

  • 6 个,Error

  • 7 个,Error

  • 8 个,"\world"

  • 。。。

我们换个思路,把 JSON.parse 去掉,只输出 JavaScript 字符串:

 
   
   
 
  1. > 'hello'

  2. "hello"

  3. > '\hello'

  4. "hello"

  5. > '\\hello'

  6. "\hello"

  7. > '\\\hello'

  8. "\hello"

  9. > '\\\\hello'

  10. "\\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() 函数关键代码:

 
   
   
 
  1. case '"':

  2. case '\'':

  3.  token = ScanString();

  4. break;

是一个很长的 switch 语句:如果遇到双引号( ")、单引号( ')则调用 ScanString() 函数。

简单解释下:以上代码是 C++ 代码,在 C++ 中单引号是字符,双引号是字符串。所以表示字符时,双引号不需要转义,但是单引号需要转义;而表示字符串时,正好相反。此处的 C++ 转义并不是我们今天要研究的转义。

ScanString() 函数中我们也只看重点代码:

 
   
   
 
  1. while (c0_ != quote && c0_ != kEndOfInput && !IsLineTerminator(c0_)) {

  2.  uc32 c = c0_;

  3.  Advance();

  4.  if (c == '\\') {

  5.    if (c0_ == kEndOfInput || !ScanEscape<false, false>()) {

  6.      return Token::ILLEGAL;

  7.    }

  8.  } else {

  9.    AddLiteralChar(c);

  10.  }

  11. }

  12. if (c0_ != quote) return Token::ILLEGAL;

  13. literal.Complete();

如果已经到了末尾,或者下 1 个字符是不能转义的字符,则返回 Token::ILLEGAL。那么我们看看 ScanEscape 是不是返回了 false 呢?

 
   
   
 
  1. template <bool capture_raw, bool in_template_literal>

  2. bool Scanner::ScanEscape() {

  3.  uc32 c = c0_;

  4.  Advance<capture_raw>();

  5.  // Skip escaped newlines.

  6.  if (!in_template_literal && c0_ != kEndOfInput && IsLineTerminator(c)) {

  7.    // Allow escaped CR+LF newlines in multiline string literals.

  8.    if (IsCarriageReturn(c) && IsLineFeed(c0_)) Advance<capture_raw>();

  9.    return true;

  10.  }

  11.  switch (c) {

  12.    case '\'':  // fall through

  13.    case '"' :  // fall through

  14.    case '\\': break;

  15.    case 'b' : c = '\b'; break;

  16.    case 'f' : c = '\f'; break;

  17.    case 'n' : c = '\n'; break;

  18.    case 'r' : c = '\r'; break;

  19.    case 't' : c = '\t'; break;

  20.    case 'u' : {

  21.      c = ScanUnicodeEscape<capture_raw>();

  22.      if (c < 0) return false;

  23.      break;

  24.    }

  25.    case 'v':

  26.      c = '\v';

  27.      break;

  28.    case 'x': {

  29.      c = ScanHexNumber<capture_raw>(2);

  30.      if (c < 0) return false;

  31.      break;

  32.    }

  33.    case '0':  // Fall through.

  34.    case '1':  // fall through

  35.    case '2':  // fall through

  36.    case '3':  // fall through

  37.    case '4':  // fall through

  38.    case '5':  // fall through

  39.    case '6':  // fall through

  40.    case '7':

  41.      c = ScanOctalEscape<capture_raw>(c, 2);

  42.      break;

  43.  }

  44.  // Other escaped characters are interpreted as their non-escaped version.

  45.  AddLiteralChar(c);

  46.  return true;

  47. }

这个函数只有 2 处返回了 false

1、如果转义字符后面是 uu 后面不是 Unicode 字符时,返回 false

2、如果转义字符后面是 xx 后面不是十六进制数字时,返回 false

也就是说: '\u''\uhello''\u1''\x''\xx' 都抛出异常。

 
   
   
 
  1. Uncaught SyntaxError: Invalid Unicode escape sequence

 
   
   
 
  1. Uncaught SyntaxError: Invalid hexadecimal escape sequence

而其它非转义字符,都直接执行了后面的代码:

 
   
   
 
  1. AddLiteralChar(c);

  2. return true;

前面的注释也说明了这一点:

 
   
   
 
  1. // 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 错误 [重复]

安全地处理节点中的错误 JSON.parse()

JSON.parse:意外字符

使用 JSON.parse 方法解析数据时 JSON 中位置 0 错误的意外令牌'

JSON.Parse 在 Faker.js 中将字符串解析为 JSOn 时在位置 0 显示错误