Rounin
Rounin

Reputation: 29463

Can I preserve the order of a javascript object's entries, when some entry keys are integers?

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

Answers (2)

the Hutt
the Hutt

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

Rounin
Rounin

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:

1

1.

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

Related Questions