Reputation: 29463
I am working with an object
where I need to preserve the order of the entries
, even though some keys
are alphanumeric and others are integers. (Yes, I know.)
The object I'm starting with looks like this:
{
a: 'the',
quick: 'quick',
b: 'brown',
fox: 'fox'
}
After manipulation, the object should look like this:
{
a: 'the',
0: 'quick',
b: 'brown',
1: 'fox'
}
But. Because iteration order in javascript objects differs from insertion order (integers are iterated first), if I go about this straightforwardly, I won't get the correctly ordered result:
let myReindexedObject = {};
myReindexedObject['a'] = 'the';
myReindexedObject['0'] = 'quick';
myReindexedObject['b'] = 'brown';
myReindexedObject['1'] = 'fox';
console.log(myReindexedObject);
I've tried to solve this issue by building a Map
(which, unlike an object
, preserves entry order) which I can then convert into an object
.
Source: (I adapted this gist by Luke Horvat: Convert ES6 Map to Object Literal .)
Can you guess what happens?
let myMap = new Map();
myMap.set('a', 'the');
myMap.set('0', 'quick');
myMap.set('b', 'brown');
myMap.set('1', 'fox');
let myArray = Array.from(myMap);
let myReindexedObject = myArray.reduce((myReindexingObject, [key, value]) => {
return Object.assign(myReindexingObject, { [key]: value })
}, {});
console.log(myReindexedObject);
Is there any way I can use integer-based keys
like 0
and 1
and still preserve the object
entries in a custom order?
Or do I need to consider other approaches?
Upvotes: 2
Views: 4347
Reputation: 18408
We can define our own object, that keeps track of properties. And by intercepting required features we can make it work.
Using Proxy it's easily achievable:
// DEMO
let o = new CoolObject();
o['a'] = 'the';
o['0'] = 'quick';
o['b'] = 'brown';
o['1'] = 'fox';
o['c'] = 'jumped';
delete o['c'];
console.log('Object.keys: ', Object.keys(o));
console.log('JSON.stringify: ', JSON.stringify(o));
console.log('console.log: ', o);
console.log('Object.getOwnPropertyNames: ', Object.getOwnPropertyNames(o));
console.log('obj.propertyIsEnumerable("keys"): ', o.propertyIsEnumerable('keys'));
console.log('obj.propertyIsEnumerable("a"): ', o.propertyIsEnumerable('a'));
<script src="https://cdn.jsdelivr.net/gh/OnkarRuikar/temp@main/CoolObject.js"></script>
See console logs for output.
Note the insertion ordered property names. Result of getOwnPropertyNames
are also insertion ordered except methods.
The CoolObject class definition:
(function () {
// original functions
let _keys = Object.keys;
let _getOwnPropertyNames = Object.getOwnPropertyNames;
let _defineProperty = Object.defineProperty;
let _stringify = JSON.stringify;
let _log = console.log;
// main feature definition
let CoolObject = function () {
let self = this;
let handler = {
_coolKeys: [],
set(target, key, val) {
let keys = this._coolKeys;
if (!keys.some(k => k === key))
keys.push(key);
target[key] = val;
},
get(target, key) {
return target[key];
},
keys() {
return this._coolKeys.slice(0);
},
deleteProperty(target, key) {
let keys = this._coolKeys;
const index = keys.indexOf(key);
if (index > -1) {
keys.splice(index, 1);
}
delete target[key];
},
defineProperty(obj, prop, val) {
let keys = this._coolKeys;
if (!keys.some(k => k === prop))
keys.push(prop);
_defineProperty(self, prop, val);
},
getOwnPropertyNames(obj) {
let props = _getOwnPropertyNames(obj);
return [...new Set([...this._coolKeys, ...props])];
},
// many improvements can be done here
// you can use your own modified pollyfill
stringifyHelper(obj, replacer, space) {
let out = '{';
for (let key of this._coolKeys) {
out += `"${key}":${_stringify(obj[key], replacer, space)}, `;
}
out += '}';
return out;
},
};
_defineProperty(self, 'keys', { value: () => handler.keys() });
_defineProperty(self, 'getOwnPropertyNames', { value: (o) => handler.getOwnPropertyNames(o) });
_defineProperty(self, 'stringify', { value: (...args) => handler.stringifyHelper(...args) });
return new Proxy(self, handler);
} // CoolObject end
// ----- wrap inbuilt objects -----
Object.keys = function (obj) {
if (!(obj instanceof CoolObject))
return _keys(obj);
return obj.keys();
}
Object.defineProperty = function (obj, prop, val) {
if (!(obj instanceof CoolObject))
_defineProperty(...arguments);
obj.defineProperty(...arguments);
}
Object.getOwnPropertyNames = function (obj) {
if (!(obj instanceof CoolObject))
return _getOwnPropertyNames(obj);
return obj.getOwnPropertyNames(obj);
}
JSON.stringify = function (obj, replacer, indent) {
if (!(obj instanceof CoolObject))
return _stringify(...arguments);
return obj.stringify(...arguments);
}
console.log = function () {
let myArgs = [];
for (let arg of arguments) {
if (arg instanceof CoolObject) {
let keys = arg.keys();
arg = Object.assign({}, arg);
for (let key of keys) {
arg[`.${key}`] = arg[key]
delete arg[key];
}
}
myArgs.push(arg);
}
_log(...myArgs);
}
window.CoolObject = CoolObject;
})();
The handler
object maintains property names in _coolKeys
array. And tracks addition and deletion operations. To make object behave like an original Object we need to wrap some inbuilt APIs, like Object.keys().
Note: for the demo I've implemented bare minimum rough code. Many improvements can be done. You can intercept more inbuilt APIs as per your requirements.
Upvotes: 3
Reputation: 29463
In the process of writing the question above, it suddenly occurred to me as I was typing:
(integers are iterated first)
that what a javascript engine recognises as an integer
and what humans recognise as a number are, of course, not the same.
To any human, these two:
are not typographically identical, but they are pretty much equivalent.
To any javascript interpreter, they are entirely distinct: the first is an integer; the second is not.
Working Example:
let myReindexedObject = {};
myReindexedObject['a'] = 'the';
myReindexedObject['0.'] = 'quick';
myReindexedObject['b'] = 'brown';
myReindexedObject['1.'] = 'fox';
console.log(myReindexedObject);
If the javascript interpreter needs to identify these indexes, it can do so, using the regex:
/d+\./
and, once identified, if it needs to know the integer that the string
corresponds to, it can use:
parseInt(myIndex);
I will use this approach for now.
If anyone can suggest a better approach, I will be happy to upvote and accept.
Upvotes: 3