Frontend Development 16 min read

Why Object.keys() Doesn’t Preserve Insertion Order: A Deep Dive into JavaScript Engine Sorting

This article investigates why JavaScript's Object.keys() may return keys in an unexpected order, explains the specification‑defined sorting rules for integer and string properties, demonstrates the issue with timestamp tags on iOS and Android, and explores the actual implementations in QuickJS and V8 engines.

Tencent IMWeb Frontend Team
Tencent IMWeb Frontend Team
Tencent IMWeb Frontend Team
Why Object.keys() Doesn’t Preserve Insertion Order: A Deep Dive into JavaScript Engine Sorting

1. Background

While maintaining a WebView page that uploads photos taken via native code, a bug was discovered where images uploaded on iOS appeared in a different order than on Android.

JS stores each image’s information in an

Object

using a tag as the key; the tag is a Unix timestamp for new photos.

Previously uploaded images use a SHA‑256 hash of the URL as the tag.

When submitting,

Object.keys()

is used to retrieve all tags and map them to URL strings.

The Android callback provides a millisecond‑level timestamp, while iOS provides a second‑level timestamp. Because the iOS tag is a shorter numeric string, it is treated as a non‑integer key and is inserted at the front of the resulting list.

<code>function getNewUrlList(oldTagUrlMap, newUrl, newTag) {
  const newMap = {
    ...oldTagUrlMap,
    [newTag]: newUrl,
  };
  return Object.keys(newMap).map(tag => newMap[tag]);
}

const originTagUrlMap = {
  'aaaaa': "https://xxx/1.jpg",
  'bbbbb': "https://xxx/2.jpg",
};
const newUrl = "https://xxx/3.jpg";
const newTagAndroid = "1612076930661"; // millisecond timestamp
const newTagIOS = "1612076930"; // second timestamp

console.log(getNewUrlList(originTagUrlMap, newUrl, newTagAndroid));
console.log(getNewUrlList(originTagUrlMap, newUrl, newTagIOS));
</code>
<code>[ "https://xxx/1.jpg", "https://xxx/2.jpg", "https://xxx/3.jpg" ] // Android
[ "https://xxx/3.jpg", "https://xxx/1.jpg", "https://xxx/2.jpg" ] // iOS
</code>

Thus Object.keys() does not always preserve insertion order . A separate array of tags can be used to guarantee order.

2. Object.keys() Sorting Mechanism

integer properties are sorted, others appear in creation order.

The Modern JavaScript Tutorial summarises that numeric keys are sorted, while other keys keep their creation order.

When the abstract operation EnumerableOwnNames is called with Object O the following steps are taken: … Return the list of names. NOTE The order of elements in the returned list is the same as the enumeration order used by a for‑in statement.

ECMAScript 6 defines the algorithm

[[OwnPropertyKeys]]

which produces keys in three phases:

All integer indices in ascending numeric order.

All remaining string keys in creation order.

All Symbol keys in creation order.

3. When Does a Key Count as an “Integer”?

Experimentation showed that keys whose numeric value is ≤ 4294967295 (2³²‑1) are treated as array indices and therefore sorted. Larger numeric strings are handled as ordinary strings and keep insertion order.

4. JavaScript Engine Implementations

QuickJS

In

quickjs.c

, the function

JS_AtomIsArrayIndex

determines whether a property key is an array index, and

is_num_string

validates the numeric range.

<code>static BOOL JS_AtomIsArrayIndex(JSContext *ctx, uint32_t *pval, JSAtom atom) {
    if (__JS_AtomIsTaggedInt(atom)) {
        *pval = __JS_AtomToUInt32(atom);
        return TRUE;
    } else {
        // ... check string representation ...
    }
}
</code>

During property enumeration, QuickJS sorts the collected array‑index keys with

rqsort

before returning the final list.

<code>if (num_keys_count != 0 && !num_sorted) {
    rqsort(tab_atom, num_keys_count, sizeof(tab_atom[0]), num_keys_cmp, ctx);
}
</code>

V8

V8 separates integer keys into a sorted list called

elements_

and stores string keys in

string_properties_

. The class

KeyAccumulator

handles this separation and sorting.

<code>bool String::AsArrayIndex(uint32_t* index) {
  uint32_t field = hash_field();
  if (IsHashFieldComputed(field) && (field & kIsNotArrayIndexMask)) {
    return false;
  }
  return SlowAsArrayIndex(index);
}
</code>

The actual sorting logic lives in

KeyAccumulator

, which first adds integer keys in ascending order, then appends string and Symbol keys in creation order.

5. Summary

Since ES6,

Object.keys()

sorts keys that are valid array indices (numeric strings ≤ 2³²‑1) before other keys; relying on insertion order is unsafe when numeric strings are involved.

V8 implements this by maintaining a sorted list of integer keys, while QuickJS uses a simple

rqsort

on detected array indices.

For business logic that requires stable ordering, maintaining an explicit array of keys is the most reliable approach.

JavaScriptV8QuickJSsortingengine internalsObject.keysarray index
Tencent IMWeb Frontend Team
Written by

Tencent IMWeb Frontend Team

IMWeb Frontend Community gathering frontend development enthusiasts. Follow us for refined live courses by top experts, cutting‑edge technical posts, and to sharpen your frontend skills.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.