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 напрямую интерпретирует символ, который нельзя экранировать, как соответствующую неэкранированную версию.