JavaScript
AdventCalendar
JSON
初心者向け
0

オブジェクトの比較に JSON.stringify() を使ってはいけない —— プロパティには順序が無い

たまたま動いているコード

オブジェクト同士の比較に JSON.stringify() を使う例がそこかしこで見られます。
典型的には、

JSON.stringify(objA) === JSON.stringify(objB)

のようなコードです。
オブジェクトの中身を再帰的にたどって比較する、いわゆる「深い比較」で「deep equality」を判定したい場合に使われる事が多いようですが、これはとても危うく、いつ壊れてもおかしくないコードです。

理由

まず、JSON化する際にそのまま含まれないプロパティ(enumerableでなかったり、値が関数であったりするものなど)がありますが、これは考慮の上でのことが多いでしょう
問題は、配列でないJavaScriptのオブジェクトには順序が無く、JSONにされる際にもプロパティの順序がどうなるか決まっていないことです。
MDNには、

配列でないオブジェクトのプロパティは、特定の順序で文字列化されることを保証されてはいません。文字列化において同じオブジェクトのプロパティの順序付けを信用しないでください。

JSON.stringify({ x: 5, y: 6 });
// '{"x":5,"y":6}' または '{"y":6,"x":5}'

とあります。
これは、同じ値の同名のプロパティしか持たないオブジェクトであっても、異なった文字列に変換され得る、ということです。
生成されたJSON同士を文字列として比較して、オブジェクトの比較に代えることは出来ないことになります。

実際の例

例えば、

let objA = {}
objA.x = 0
objA.y = 0

let objB = {}
objB.y = 0
objB.x = 0

console.log( JSON.stringify(objA) === JSON.stringify(objB) )

を考えます。
objAobjB はJSON化の対象となるプロパティ、xy を持っています。
それぞれ値も同じですので、これは比較結果として、true(等しい)を期待しています。
しかし、Node.js v6.11.4での結果は false(等しくない)です。
生成されたJSONは、

// objA
'{"x":0,"y":0}'

// objB
'{"y":0,"x":0}'

となっています。
この動作は以下のように説明できると思います。

  • objAobjB ではプロパティを追加した順序が違う
  • 現在のNode.jsでは for..in などでのプロパティの列挙が、プロパティを追加した順序で行なわれる
  • 現在のNode.jsでは JSON.stringify() でJSON化すると、プロパティが列挙される順序で出現する

しかし、これはあくまでも、たまたまこのバージョンのNode.jsではこのようになっていた、という話でしかないことに注意して下さい。

プロパティを追加する順序に依存するなら、同じ順序で追加するように気を付ければよい

と考えては危険です。
将来は動作が変わるかも知れません(プロパティを追加する順序に依存するコードを将来に渡ってメンテするというのもちょっと面倒ですね)。

代案

では深い比較をしたい時はどうすればよいかというと、残念ながら簡単な方法はありません
利用できるものとして、

などがありますので参考にしてください。
オブジェクト同士の比較方法に決まりはありませんので、同じ deep equal を名乗っていても仕様は少し異なることがあります。
JSON化に際し、プロパティの順序を常に一定にするというアプローチもあります。

実際の所、本当に深い比較が必要なことは少ないので、そこを見直してみるのもよいでしょう。

この記事のライセンス

クリエイティブ・コモンズ・ライセンス
この記事はCC BY 4.0(クリエイティブ・コモンズ 表示 4.0 国際 ライセンス)の元で公開します。