Does Javascript Object Keys Guarantee Stable Key Order
# Why We Need Stable Orders of Object Keys
Obviously, If we need an "ordered data structure", an array will be used, such as ['^.svg', '^.(svg|png|mp3)']
will
first
match by ^.svg
and then match by ^.(svg|png|mp3)
criteria. But, what if the "ordered data structure" is an
Javascript Object.
For example:
import jest from 'jest';
const someUserCustomizedMapper = {
'^.svg': require.resolve('svg-loader')
}
const jestConfig = {
moduleNameMapper: {
'^.(svg|png|mp3)': require.resolve('file-loader'),
...someUserCustomizedMapper
}
}
jest.runClI({
config: JSON.stringify(jestConfig)
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Does svg-loader ? The answer is NO! Maybe you can say, ah, if the jestConfig is like this(change the object creation order),
const jestConfig = {
moduleNameMapper: {
...someUserCustomizedMapper,
'^.(svg|png|mp3)': require.resolve('file-loader'),
}
}
2
3
4
5
6
Right, it would be as we expected result.
But Why and always True
?
# Why and How
First, JSON.stringify convert Object
to String
JSON.stringify use es 6 runtime method SerializeJSONProperty
[^2] to serialize object, then the serializer parse object
to string use below rules[^3]:
If value has a [[NumberData]] internal slot, then
Let value be ToNumber(value).
ReturnIfAbrupt(value).
Else if value has a [[StringData]] internal slot, then
Let value be ToString(value).
ReturnIfAbrupt(value).
Else if value has a [[BooleanData]] internal slot, then
Let value be the value of the [[BooleanData]] internal slot of value.
2
3
4
5
6
7
8
Second, when jest use config, it would be convert the object like String
to Javascript Object
. Similar as above,
transform data by use InternalizeJSONProperty
and EnumerableOwnNames
, and the rules[^4]:
...
Let keys be EnumerableOwnNames(val).
For each String P in keys do,
...
2
3
4
The important is EnumerableOwnNames
[^5] function
When the abstract operation EnumerableOwnNames is called with Object O the following steps are taken:
1. Assert: Type(O) is Object.
2. Let ownKeys be O.[[OwnPropertyKeys]]().
3. ReturnIfAbrupt(ownKeys).
4. Let names be a new empty List.
5. Repeat, for each element key of ownKeys in List order
If Type(key) is String, then
Let desc be O.[[GetOwnProperty]](key).
ReturnIfAbrupt(desc).
If desc is not undefined, then
If desc.[[Enumerable]] is true, append key to names.
6. Order the elements of names so they are in the same relative order as would be produced by the Iterator that would
be returned if the [[Enumerate]] internal method was invoked on O.
7. Return names.
Note: The order of elements in the returned list is the same as the enumeration order that is used by a for-in
statement.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
And last the [[OwnPropertyKeys]]
[^6] is stable ordered rules:
When the [[OwnPropertyKeys]] internal method of O is called the following steps are taken:
1. Let keys be a new empty List.
2. For each own property key P of O that is an integer index, in ascending numeric index order
Add P as the last element of keys.
3. For each own property key P of O that is a String but is not an integer index, in property creation order
Add P as the last element of keys.
4. For each own property key P of O that is a Symbol, in property creation order
Add P as the last element of keys.
5. Return keys.
2
3
4
5
6
7
8
9
This is JSON how to process Javascript object and also the same as Javascript Object.keys
, Reflect.ownKeys
and
for...in
loop[^7]
To briefing the rule is:
- Integer or String Integer
- Other String
- Symbol Also you can try these code on Chrome newest version,
// ROUND I
let a = {'#': 0, 1: 0, '$':0, f: ()=>{}, '2':0}
Object.keys(a); // result: ["1", "2", "#", "$", "f"]
Object.getOwnPropertyNames(a); // result: ["1", "2", "#", "$", "f"]
let r1 = []; for(const k in a) r1.push(k) // result: ["1", "2", "#", "$", "f"]
// ROUND II
a[Symbol('s')] = 0;
a["@"] = 0;
a[3] = 0;
Object.keys(a); // result: ["1", "2", "3", "#", "$", "f", "@"]
Object.getOwnPropertyNames(a); // result: ["1", "2", "3", "#", "$", "f", "@"]
let r2 = []; for(const k in a) r2.push(k) // result: ["1", "2", "3", "#", "$", "f", "@"]
2
3
4
5
6
7
8
9
10
11
12
13
Conclusion, the object orders in ES6 is depend on the order when you create object and also depend on the type
(Integer, String or Symbol).
# Always True
?
# ECMAScript Version
This ordered behaviour is release by ES6 information from Allen Wirfs Brock, Project Editor of the ECMAScript 2015 Language Specification[^7], means under ES6(ECMAScript 2015) it is not guarantee.
In ES5, objects were intrinsically unordered[^8]. ES3 was explicit about that an "object is an unordered collection of properties", the same thing is still in JSON for example, where objects are defined as "unordered set of name/value pairs". ES5 did only specify that for-in and Object.keys should use the same order ("if an implementation specifies one" at all). ES6 didn't even tighten this, it only describes it with a new [[enumerate]]-mechanism.
Additional Reading: Some other post about ES5 Object Key Orders (opens new window)
# Browser Support
For latest version of browser, it is fully support ES6 standard language. More detail, please check this link BuildIn Property Support (opens new window)
# Javascript Proxy Object
The proxy object is mostly same behaviour as build-in Object. But not always.
let a = {'#': 0, 1: 0, '$':0, f: ()=>{}, '2':0}
const p1 = new Proxy(a, {});
Object.keys(p1); // result: ["1", "2", "#", "$", "f"]
Object.getOwnPropertyNames(p1); // result: ["1", "2", "#", "$", "f"]
let r1 = []; for(const k in p1) r1.push(k) // result: ["1", "2", "#", "$", "f"]
const p2 = new Proxy(a, { ownKeys: () => ['3', '1', '2']})
console.log(p2); // result: {1: 0, 2: 0, 3: 0, #: 0, $: 0, f: ƒ, @: 0, Symbol(s): 0}
Reflect.ownKeys(p2); // result: ["3", "1", "2"]
Object.keys(p2); // result: ["3", "1", "2"]
Object.getOwnPropertyNames(p2); // result: ["3", "1", "2"]
2
3
4
5
6
7
8
9
10
11
# Reference
- ES6 Official Doc (opens new window)
- ES6 JSON.stringify (opens new window)
- ES6 SerializeJSONProperty (opens new window)
- ES6 JSON.parse (opens new window)
- ES6 EnumerableOwnNames (opens new window)
- ES6 OwnPropertyKeys (opens new window)
- ES6 ordered keys discussion (opens new window)
- ES5 Object.keys (opens new window)