从一个 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 遇到不能转义的字符直接解释为对应的非转义版本。


公众号回复 V8 查看更多 V8 专题文章。


在使用JavaScript的`JSON.parse()`函数解析多层嵌套的JSON字符串时,可能会遇到报错的情况。常见的报错信息可能是"JSON.parse: end of data after property starting at line..."或者"JSON.parse: unexpected character at line..."等。 解决这个问题的方法有以下几种: 1. 确保JSON字符串的格式正确:在进行JSON字符串的嵌套时,必须确保每一层的JSON字符串都是有效的。可以使用在线的JSON验证工具或者自行编写代码来验证JSON字符串是否符合标准的JSON格式。 2. 检查字符串中的特殊字符:特殊字符,例如回车符、制表符、斜杠和引号等,可能会导致JSON.parse()函数无法正确解析字符串。需要在解析之前,对字符串中的特殊字符进行转义或修改。 3. 使用JSON.parse()的第二个参数:JSON.parse()函数的第二个参数可以是一个“reviver”函数,用来在解析过程中修改解析出来的值。通过使用这个参数,可以自定义解析过程,处理特殊情况或者修复无法解析的值。 4. 递归解析嵌套的JSON:如果JSON字符串是多层嵌套的,需要使用递归的方法进行解析。首先将最外层的JSON字符串解析,然后再递归解析内层的JSON字符串,直到解析完成。 综上所述,要解决多层嵌套JSON字符串报错的问题,我们需要确保JSON字符串格式正确,检查特殊字符,使用合适的参数和递归的方式解析嵌套的JSON字符串。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值