Objects do retain the order that their (non-numeric) keys were inserted in, but they are only guaranteed to iterate in that order using certain methods. Per the specification, Object.keys
and its variants, JSON.stringify
, and for..in
loops all iterate in an unspecified order. These methods all call EnumerateObjectProperties, which explicitly states:
The mechanics and order of enumerating the properties is not specified
While environments generally iterate in the predictable order anyway for those methods, that behavior is in no way guaranteed by the specification.
But, Object.getOwnPropertyNames
(and Reflect.ownKeys
, and Object.getOwnPropertySymbols
) are guaranteed to iterate in a particular order: ascending numeric keys, followed by other keys in insertion order, per [[OwnPropertyKeys]]
.
So, a specification-guaranteed method of logging (non-numeric) properties in insertion order will involve using one of the above methods, rather than Object.keys
or its variants:
var obj = {
z: 1,
t: 2,
y: 3,
a: 4,
n: 5,
k: 6
};
const str = '{\n' +
Object.getOwnPropertyNames(obj).map(key => ` ${key}: ${obj[key]}`).join('\n')
+ '\n}';
console.log(str);
For nested objects, you'll need a recursive function instead:
var obj = {
z: 1,
t: 2,
y: 3,
a: 4,
nested: {
foo: 9,
bar: 99,
baz: 35
},
n: 5,
k: 6
};
const objToArrOfLines = (obj, lines=[], leftPadding=0, keyForThisObj) => {
lines.push(`${' '.repeat(leftPadding)}${keyForThisObj ? keyForThisObj + ': ' : ''}{`);
Object.getOwnPropertyNames(obj).forEach((key) => {
const val = obj[key];
if (typeof val === 'object') {
objToArrOfLines(val, lines, leftPadding + 2, key);
} else {
lines.push(`${' '.repeat(leftPadding + 2)}${key}: ${val}`);
}
});
lines.push(`${' '.repeat(leftPadding)}}`);
return lines;
};
const objToStr = (obj) => {
console.log(objToArrOfLines(obj).join('\n'));
};
objToStr(obj);