Изучение экранирующих символов JavaScript из-за ошибки JSON.parse

внешний интерфейс JSON JavaScript C++
Изучение экранирующих символов JavaScript из-за ошибки JSON.parse

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, "мир"
  • 2, ошибка
  • 3, ошибка
  • 4, "\ мир"
  • 5, "\ мир"
  • 6, ошибка
  • 7, ошибка
  • 8, "\\ мир"
  • . . .

Давайте передумаем, удалим JSON.parse и будем выводить только строки JavaScript:

> 'hello'
"hello"
> '\hello'
"hello"
> '\\hello'
"\hello"
> '\\\hello'
"\hello"
> '\\\\hello'
"\\hello"

Вероятно, проблема найдена.

Внесите вышеуказанные правила в предыдущий код JSON.parse, и проблема будет решена.

Давайте посмотрим на правила парсинга строк JSON:

Согласно этому правилу, давайте разберем «\hello», первый символ — обратная косая черта (\), поэтому берем самую нижнюю ветвь (отмеченную красной линией) после кавычки:

Второй символ — h, но после обратной косой черты всего 9 путей, и это никакому пути не принадлежит, так что это недопустимый символ.

Не только JSON, ошибки типа Error:(7, 27) Illegal escape: '\h' возникают на многих языках.

Но я не знаю, почему JavaScript может анализировать этот недопустимый escape-символ, и решение тоже очень жестокое: игнорировать его напрямую.

В спецификации es я не нашел конкретного раздела. Давайте посмотрим, как V8 анализирует его.

После того, как движок прочитает исходный код JavaScript, он сначала выполняет лексический анализ, а файл/src/parsing/scanner.ccФункция заключается в чтении исходного кода и его разборе (последняя версия 6.4.286).

Найдите ключевой код функции Scanner::Scan():

case '"':
case '\'':
  token = ScanString();
break;

является очень длинным оператором переключения: если встречаются двойные (") или одинарные (') кавычки, вызывается функция 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();

Возвращает токен :: Незаконный, если он был в конце концов, или следующий символ - это символ, который не может быть сбежен. Тогда давайте посмотрим, если сканер возвращает 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;
}

Эта функция возвращает false только в двух местах.

1. Если за escape-символом следует 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 по-разному обрабатывают escape-символы, что приводит к трудно обнаруживаемым ошибкам. JSON выдает исключение, когда встречает символ, который нельзя экранировать, в то время как JavaScript напрямую интерпретирует символ, который нельзя экранировать, как соответствующую неэкранированную версию.