JavaScriptクイズ: JSON評価時の挙動
Xで出したクイズの解説です。
問題は次の通りです。
以下のJavaScriptの比較は常にtrueになるでしょうか? それともfalseになる可能性があるでしょうか? (そして、あるとしたらそれはどのような場合でしょうか?)
JSON.stringify(
eval(
"(" + JSON.stringify(x) + ")"
)
) === JSON.stringify(x)
ただし、以下の条件を仮定します。
- Objectなどのビルトインの定義は汚染されていないものとし、evalはglobalThis.evalを指しているものとします
- 内側のstringifyがエラーになる場合は除外します
答え
この出題をわざわざすることからだいたい予想がついてしまうと思いますが、これは「falseになる可能性がある」というのが正しいです。
問題は、具体的にどのような場合があるかです。
これについて、当初想定していた解は2種類ありましたが、興味深い非作為解もいくつかあったので一緒に紹介します。
非作為解1: 副作用
JSON.stringify(x)
が2回評価されていることに着目した解法です。 JSON.stringify
内でユーザー定義の処理が呼ばれる機会はいくつかあり、その中で副作用を発行することで問題の定義を満たしたまま出力を変えることができます。
たとえばtoJSONを利用すれば以下のようにすることができます。
const x = { toJSON() { delete this.toJSON; } };
以降では JSON.stringify(x)
を1回しか評価しないようにしても成立する解答を考えます。
非作為解2: スコープ
補足条件でスコープの汚染を部分的に禁止していましたが、以下の2つの汚染を禁止していませんでした。
- JSON
- undefined
これらがスコープ内の同名の別の変数で隠されている場合は、異なる挙動をさせることができます。
JSON
については言うまでもありませんが、 undefined
の上書きでも異なる挙動をさせることができます。これは以下のメカニズムによるものです。
-
JSON.stringify(undefined)
はundefined
-
"(" + undefined + ")"
は"(undefined)"
-
"(undefined)"
をevalすると通常はundefined
に評価されるが、別の変数に隠されている場合は別の値になる可能性がある。
__proto__
想定解1: JavaScriptのオブジェクト式の評価では __proto__
に関する特別ルールがあります。それは、Computed Property Nameを使わずに定義されたプロパティの名前が __proto__
である場合、それは __proto__
というプロパティの作成ではなく [[Prototype]]
内部スロットの設定として振る舞うというものです。
なお、 JSON.parse()
の挙動は対応するJavaScript式の評価とほぼ同じものとして定義されていますが、この場合に限り __proto__
に関する特別ルールは明示的に無効化するように定義されているため、これは eval
特有の問題ということになります。これが作題意図の想定解です。
const x = { ["__proto__"]: {} };
想定解2: キーの順序
JavaScriptのオブジェクトは通常、キーを挿入順で保ちます。したがってほとんどの場合はJSONからパースしたキー順序はオブジェクト内でも保たれます。
しかし、オブジェクトのデフォルト挙動では"0"
, "1"
, "2"
, ... のようなキーについては他のキーより前に数値の昇順で並べるという特別なルールがあり、これを利用することが可能です。
> { 2: 2, 1: 1}
{ '1': 1, '2': 2 }
つまり、以下のように整数キーの順序が逆転した状態のJSONをパースさせれば、JSON上の順序を忘れさせることができます。
> JSON.parse("{\"2\":2,\"1\":1}")
{ '1': 1, '2': 2 }
問題は、そのようなJSONを JSON.stringify
にどのように生成させるからです。当然、普通のオブジェクトの挙動とは異なる [[OwnPropertyKeys]]
を渡さなければ、このような特殊なJSONを生成させることはできません。
そこで使えるのがProxyです。
Proxyの [[OwnPropertyKeys]]
は、handlerが返したキーの配列をフィルタリングして返す形で定義されており、順序はオリジナルのオブジェクトではなくhandlerの指定が優先されます。したがって、ここで明示的に逆転したキー順序を指定すれば、通常は作れないようなJSONを生成させることができることになります。
こちらの想定解は、作題後の別解検討時に出てきたものです。正解した皆さん、おめでとうございます 🎉
const x = new Proxy({ 1: 1, 2: 2 }, { ownKeys() { return ["2", "1"]; } });
このクイズを見た人へのおすすめ
こちらもどうぞ
Discussion