10
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

JavaScript における Object の列挙順は保証されない

Last updated at Posted at 2024-09-03

はじめに

今回、自身の担当するプロジェクトにおいて不具合が見つかり、その要因が 「Object の列挙順は定義順になる、というのを前提としたコードを書いていた」ことにありました。
あまりちゃんと考えずにこうしていたのですが、この認識は誤りです。
これについて少し自分でも動作を確認してみてまとめました。

なおここで保証する・しないというのは ECMAScript の仕様上、保証されるかどうかというコンテキストでの表現であるとご理解ください。

どういうことなのかを簡単に例示します。

Node: v20.8.1

  const obj = {
    3: '3',
    1: '1',
    9: '9',
    5: '5',
  }

// { '1': '1', '3': '3', '5': '5', '9': '9' }

宣言時は key の順が 3, 1, 9, 5 となるように宣言しているのですが、参照すると 1, 3, 5, 9 の順番になってしまっています。
これが 3, 1, 9, 5 となる前提で順番に依存した実装をしてしまっていたため不具合を招いていました。

実際の実装では Object.entries(obj) で得た配列を for ... of でループ処理していたのですが、当然この場合も順番が定義順であることは保証されていないようでした。

for (const [key, _val] of Object.entries(obj)) {
  console.log('key', key)
}

// key 1
// key 3
// key 5
// key 9

なんとなく並び順からわかるように、少なくともこの Node 環境下では数字(整数)は数値の昇順で並んでいるようでした。

キーが整数でない文字列の場合

では続いて整数ではない、一般的な文字列で試してみます。

const obj = {
  dog: 'D',
  cat: 'C',
  monkey: 'M',
  elephant: 'E',
}

// { dog: 'D', cat: 'C', monkey: 'M', elephant: 'E' }

このときは定義順になっていました。アルファベット順(辞書順)というわけでもなさそうです。

キーが小数の場合

const obj = {
  '7': '7',
  '12.12': '12.12',
  '2': '2',
  '5.5': '5.5',
  '0.8': '0.8',
}

// { '2': '2', '7': '7', '12.12': '12.12', '5.5': '5.5', '0.8': '0.8' }

2, 7 は整数なのでこの順で、それ以外の小数とみなせる数字については定義順になってそうです。
「整数ではない文字列の場合」で確認した一般的な文字列同様に定義順に列挙されているようです。

キーに number 形式の数値と string 形式の数値を混合させる場合

const obj = {
  7: '7',
  '4': '4',
  '2': '2',
  1: '1',
}

// { '1': '1', '2': '2', '4': '4', '7': '7' }

これは想像できていましたが number だろうが string だろうが同一に扱われて数字の昇順になっています。
そもそも Object の key に number は利用できない(というか利用すると string と同等に扱われる)ので、そのことを知っていれば当然の挙動に見えます。
(なので列挙時にもすべて string として扱われていますね)

Object の列挙順についてわかること

ここまでの簡単な検証で推測できることをまとめると

  • 整数とみなせる key は数字の昇順に並ぶ(定義順よりも優先される)
  • 整数とみなせない key は宣言順に並ぶ

となりそうです。

現代の ECMAScript の仕様では、走査順序は明確に定義されており、 実装同士の間で一貫しています。プロトタイプチェーンのそれぞれの成分内では、非負の整数値(配列の添字となるもの)はすべて値の昇順で最初に走査され、次に文字列のキーがプロパティの作成時系列で昇順に走査されます。
cf. for...in JavaScript | MDN

明確に記載があることをコメントにてお知らせいただきました。

※ちなみに調べていると symbol も宣言順(ただし文字列キーの更に後ろ)という情報もみられました(未確認)

ただし 「順番は保証されない」という事実がもっとも重要 です。
特に古いブラウザや特定の JavaScript エンジンの実装によっては挙動は異なる可能性があります。
そのため、「だいたいこうなるっぽいけど違うかもしれない」くらいに捉えておくことが適切です。

オマケ1:配列は順番どおりに列挙されることが保証されるか

配列を順々に処理したいときに map()reduce() などでループ処理しますが、この順番は保証されるのか?
mapreduce においてこれは index の順に列挙されることが保証されるようでした。

また、配列は「キーが整数である Object と同義である」という特性を持っています。したがって、配列のインデックスが順序通りに処理されるという事実から、Object の整数キーが数値の昇順に並ぶ理由も理解しやすくなりそうな気がしました。

オマケ2:Object でも宣言順に列挙したい

Object を使う以上、列挙順は保証されないようなので Map を使うと良さそうです。
Map の場合は宣言順に列挙できることが保証されているようです。

  • コンストラクタで値を流し込む場合の順序が保証される 仕様
  • setで挿入する際の順序が保証される 仕様

また、Object と違って number 形式の key が許容されるので、1'1' は別物であると扱われます。

const mapObj = new Map([
  [9, '9'],
  [3, '3'],
  [1, 'num1'],
  [21, '21'],
  ['1', 'str1'],
])

mapObj.forEach((val, key) => {
  console.log('key', key, 'val', val)
})
console.log(mapObj)

// key 9 val 9
// key 3 val 3
// key 1 val num1
// key 21 val 21
// key 1 val str1
// Map(5) { 9 => '9', 3 => '3', 1 => 'num1', 21 => '21', '1' => 'str1' }

参考にさせていただいた記事

10
6
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?