JavaScriptのオブジェクトのキー順序:Object.keysで数値のキーが先に来る理由
Object.keysで数値のようなキーが追加した順番に関係なく先頭に来る理由を、ECMA-262のOrdinaryOwnPropertyKeysで説明します。配列インデックスの範囲、シンボル、削除して追加し直したキー、JSON.stringifyの順序も整理しました。
オブジェクトに "300"、"20"、"a7"、"1" の順でキーを追加して Object.keys で取り出すと、追加した順ではなく ["1", "20", "300", "a7"] が返ります。配列インデックスに当たるキーは小さい順に先頭へ集まり、残りの文字列キーはその後ろに追加した順で並びます。
const stock = {};
stock["300"] = "C";
stock["20"] = "B";
stock["a7"] = "D";
stock["1"] = "A";
console.log(Object.keys(stock)); // ["1", "20", "300", "a7"]
エンジンごとの癖ではなく、JavaScriptの標準であるECMA-262が定めた順序です。この記事は2026年10月20日に確認したECMA-262の編集者草案(ECMAScript 2027)に基づきます。コード中のコメントにある結果は、以下で説明する仕様のアルゴリズムをたどって求めた値で、記事中の例をそのままNode.js 24.13.0で実行して同じ結果になることを確かめました。
仕様が定める3つのグループ
JavaScriptのプロパティキーは文字列かシンボルのどちらかです。通常のオブジェクトが自身のキー一覧を返すときに使われる手順はOrdinaryOwnPropertyKeysで、キーを3つのグループに分けてこの順につなげます。
- 配列インデックスであるキーを、数値の小さい順に
- 配列インデックスではない文字列キーを、プロパティが作られた順に
- シンボルキーを、プロパティが作られた順に
並べ替えられるのは1つ目のグループだけで、残りの2つは作成順に従います。そのため「挿入順が保たれる」という説明は、数値のようなキーが混ざった時点で当てはまらなくなります。
「数値のキー」の正確な範囲
1つ目のグループに入るキーを、仕様は配列インデックス(array index)と呼びます。整数インデックス(integer index)のうち、数値が+0から2³² − 2までの範囲にある文字列です。仕様の注記は、2³² − 1を除くすべての32ビット符号なし整数に対応する配列インデックスがあると書き、"-0" は整数インデックスでも配列インデックスでもないと別に明記しています。
そのため、範囲のすぐ外にあるキーは数値に見えても通常の文字列キーとして扱われ、作成順の位置に残ります。
const o = {};
o["4294967295"] = "2^32 - 1";
o["-0"] = "minus zero";
o["4294967294"] = "2^32 - 2";
o["7"] = "seven";
console.log(Object.keys(o)); // ["7", "4294967294", "4294967295", "-0"]
"7" と "4294967294"(2³² − 2)は配列インデックスなので、数値の順で先頭に来ます。"4294967295"(2³² − 1)と "-0" は配列インデックスではないので、作られた順に後ろへ続きます。数値の-0が比較でどう扱われるかはNaN === NaN が false になる理由で別にまとめました。ここでの "-0" はその数値ではなく、2文字の文字列キーです。
同じ順序を使うAPI
キーの一覧を返す組み込み関数の多くは、オブジェクトの内部メソッド [[OwnPropertyKeys]] を呼び、その順序を出発点にします。通常のオブジェクトでは、このメソッドはOrdinaryOwnPropertyKeysの結果をそのまま返します。
const tag = Symbol("tag");
const obj = {};
obj.b = 1;
obj[tag] = 2;
obj["2"] = 3;
obj.a = 4;
obj["1"] = 5;
Object.defineProperty(obj, "hidden", { value: 6, enumerable: false });
console.log(Object.keys(obj)); // ["1", "2", "b", "a"]
console.log(Object.getOwnPropertyNames(obj)); // ["1", "2", "b", "a", "hidden"]
console.log(Reflect.ownKeys(obj)); // ["1", "2", "b", "a", "hidden", Symbol(tag)]
console.log(JSON.stringify(obj)); // {"1":5,"2":3,"b":1,"a":4}
Reflect.ownKeysは[[OwnPropertyKeys]]の結果を配列にして返します。列挙不可のキーもシンボルもすべて含まれます。Object.getOwnPropertyNamesとObject.getOwnPropertySymbolsは同じ一覧を先頭からたどり、文字列キーだけ、またはシンボルキーだけを残します。順序は変わりません。Object.keysとJSON.stringifyはEnumerableOwnPropertiesという手順を通り、列挙可能な文字列キーだけを使います。hiddenとシンボルが含まれないのはそのためです。Object.assignはコピー元の[[OwnPropertyKeys]]の順に、列挙可能なプロパティを1つずつコピー先へ書き込みます。
最後の点から、オブジェクトをコピーしても順序は並べ替えられません。新しいオブジェクトでも、配列インデックスのキーはまた数値の順で先頭に集まります。
値の代入なら位置はそのまま、削除して追加し直すと末尾へ
2つ目と3つ目のグループの基準は「プロパティが作られた順」です。既にあるプロパティに値を代入し直しても新しく作られるわけではないので、位置は変わりません。delete で削除してから追加し直すと、そのとき新しく作られるので末尾に移ります。これは仕様の文言から導いた結果です。
const cfg = {};
cfg.host = "a";
cfg.port = 1;
cfg.user = "u";
cfg.host = "b";
console.log(Object.keys(cfg)); // ["host", "port", "user"]
delete cfg.host;
cfg.host = "c";
console.log(Object.keys(cfg)); // ["port", "user", "host"]
出力の順序を自分で決めたいとき
ログやAPIレスポンスのキー順を固定したい場合は、JSON.stringify の第2引数にキー名の配列を渡せます。仕様はこの配列からPropertyListを作り、オブジェクトをシリアライズするときにEnumerableOwnPropertiesの代わりにこの一覧をその順で使います。同じ名前が2回あれば、最初のものだけが残ります。
const row = {};
row.name = "kim";
row["2026"] = 10;
row.id = 7;
row.meta = { z: 1, id: 2 };
console.log(JSON.stringify(row));
// {"2026":10,"name":"kim","id":7,"meta":{"z":1,"id":2}}
console.log(JSON.stringify(row, ["id", "name", "2026", "meta"]));
// {"id":7,"name":"kim","2026":10,"meta":{"id":2}}
注意点が1つあります。PropertyListは最上位のオブジェクトだけでなく、シリアライズされるすべてのオブジェクトに同じ一覧が使われます。上の例で meta の z が消えたのはそのためです。入れ子のオブジェクトのキーも一覧に入れておく必要があります。JSONで値がエラーなしに変わったり消えたりする他のケースはよくあるJSONエラーまとめに集めています。
順序そのものがデータの意味を持つなら、オブジェクトではなく配列に入れたほうが意図がはっきりします。オブジェクトのキー順は、上のルールを知っている人にしか予測できないからです。
この記事で扱っていないこと
ここまでのルールは通常のオブジェクト(ordinary object)のものです。仕様はいくつかの特殊なオブジェクトに [[OwnPropertyKeys]] を別に定義しています。Stringオブジェクトは文字列の各位置("0"、"1"、…)を先に返し、TypedArray、モジュール名前空間オブジェクト、Proxyにもそれぞれの定義があります。Proxyに ownKeys トラップがある場合は、いくつかの検査を通ったトラップの戻り値がその順のまま結果になるため、上の順序は保証されません。for...in ループの順序は今回確認した範囲に含まれていないため扱っていません。
参考資料
すべて2026年10月20日に確認しました。節番号は確認時点のECMA-262編集者草案(ECMAScript 2027)に基づきます。
- ECMA-262、6.1.7 The Object Type(プロパティキー、整数インデックス、配列インデックス)
- ECMA-262、7.3.23 EnumerableOwnProperties
- ECMA-262、10.1.11.1 OrdinaryOwnPropertyKeys
- ECMA-262、10.4.3.3 Stringオブジェクトの
[[OwnPropertyKeys]]、10.5.11 Proxyの[[OwnPropertyKeys]] - ECMA-262、20.1.2.1 Object.assign、20.1.2.11.1 GetOwnPropertyKeys、20.1.2.19 Object.keys
- ECMA-262、28.1.10 Reflect.ownKeys
- ECMA-262、25.5.4 JSON.stringify、25.5.4.5 SerializeJSONObject