🟨

JavaScriptクイズ: JSON.parseできるけどJSとして有効でないことはある?

2024/06/16に公開

以下のクイズの解説を書きます。

JavaScriptクイズ: 以下の関数はtrueを返すことがある?

function quiz(s) {
  if (typeof s !== 'string') return false;
  try { JSON.parse(s); } catch { return false; }
  try { eval('(' + s + ')'); } catch { return true; }
  return false;
}

※ ただし、 JSONeval を外のスコープから置き換える、といったことは禁止。

バージョンはES2023 (ECMA-262, 14th edition, June 2023 ECMAScript® 2023 Language Specification) を想定。ただし、さしてバージョン間の差異の影響はないと思います。


この問題は仕様を眺めながらqnighyさんの以下のクイズをふと思い出して作ったクイズです。

https://zenn.dev/qnighy/articles/4889cf4de3047f







































解答

true を返すことがある。以下がそのようなJSONの例だ。

{"__proto__":0,"__proto__":0}
console.log(quiz('{"__proto__":0,"__proto__":0}'));

解説。

まず JSON.parse(s) について。こちらの仕様は ECMA 25.5.1 JSON.parse ( text [ , reviver ] ) [1] によって定められており、簡単に書けば以下のような挙動です。

  1. s がJSONの仕様[2]にある構文を満たすか確認。満たさなければエラー
  2. '(' + s + ')' をパースして評価。ただし、このときパースと評価はキーが __proto__ のときに関して、通常の JS の評価とは少し違う動きをする。
  3. そのオブジェクトを返す。

上記の通り、 JSON.parse はそのまま JS エンジンとほぼ同じ挙動を取ります。 __proto__ に関してはレガシー指定[3]の項目を除けば2箇所で言及されています。
一つは紹介させていただいたqnighyさんのクイズでも触れられている、プロトタイプのセッターになるというもの [4]
もう一つが重複して指定した際に他のプロパティとは違いエラーになるという点です。 [5]
たとえば、 {"abc":1,"abc":2} のような単にキーが重複しているJSONは構文的には問題がなく、また少なくとも ECMAScript では曖昧性のない一意な解釈を与えています。しかし、 __proto__ がキー名の場合は、(おそらくプロトタイプセッターとしての意味を持たせているので)Computed Propertyを使わずに重複させるとエラーになります。

というわけでそこを攻めてあげれば今回のクイズは攻略可能です。

ちょっとだらないクイズかなとも思いましたが、回答者のうち24%ほどが「trueを返すことはない」と回答していたので、お役に立てば嬉しいです。

V8の実装をみてみた

ところで、構文チェックをせねばならない点と __proto__ の例外的な処理を除けば、JSON.parse はJSのパーサーを流用できるけど、実際そうしてるのかなと思ってみてみました。

// ES6 section 24.3.1 JSON.parse.
BUILTIN(JsonParse) {
  HandleScope scope(isolate);
  Handle<Object> source = args.atOrUndefined(isolate, 1);
  Handle<Object> reviver = args.atOrUndefined(isolate, 2);
  Handle<String> string;
  ASSIGN_RETURN_FAILURE_ON_EXCEPTION(isolate, string,
                                     Object::ToString(isolate, source));
  string = String::Flatten(isolate, string);
  RETURN_RESULT_OR_FAILURE(
      isolate, String::IsOneByteRepresentationUnderneath(*string)
                   ? JsonParser<uint8_t>::Parse(isolate, string, reviver)
                   : JsonParser<uint16_t>::Parse(isolate, string, reviver));
}

引用元: https://github.com/v8/v8/blob/0bbdf3cbcff39caf71055d04064ffaa080bbf56a/src/builtins/builtins-json.cc#L16-L17 より引用

そしてさらに JsonParser::Parse を見てみたところ、専用のパーサーが組まれていそうでした。パフォーマンスを重視するV8にとって、当然ということでしょうね。

というか、そういえば前に const value = {...} とJSONを書くより、 const value = JSON.parse('{...}') と書くほうが速い、という話題がありましたが、このJSON特化の実装が要因ということでしょうね。

As long as the JSON string is only evaluated once, the JSON.parse approach is much faster compared to the JavaScript object literal, especially for cold loads. A good rule of thumb is to apply this technique for objects of 10 kB or larger — but as always with performance advice, measure the actual impact before making any changes.
引用元: https://v8.dev/blog/cost-of-javascript-2019#json

意訳: JSON文字列が一度しか評価されないという前提のもとで、JSON.parseを利用する方法はJSオブジェクトリテラルを利用するよりはるかに速い。特にコールドロードにおいて。指標として、このテクニックは10kBより大きいオブジェクトに利用するとしておけばよいだろう。しかし、いかなる場合でも、パフォーマンス一般のアドバイスとして、変更する前に計測しよう。

脚注
  1. ES 25.5.1 JSON.parse ( text [ , reviver ] ) https://262.ecma-international.org/14.0/#sec-json.parse ↩︎

  2. ECMA404 https://ecma-international.org/publications-and-standards/standards/ecma-404/ ↩︎

  3. ESの仕様の中でLegacyという形で指定されているもの。利用者はそれらがエンジンに実装されていることを仮定するべきではない、としている。 ↩︎

  4. ES 13.2.5.5 Runtime Semantics: PropertyDefinitionEvaluation https://262.ecma-international.org/14.0/#sec-runtime-semantics-propertydefinitionevaluation ↩︎

  5. ES 13.2.5.1 Static Semantics: Early Errors https://262.ecma-international.org/14.0/#sec-object-initializer-static-semantics-early-errors ↩︎

GitHubで編集を提案

Discussion