ESC

Type to search the knowledge base.

Object.entries and fromEntries

Round-trip own enumerable string keys with Object.entries and Object.fromEntries. Not a Map, not symbols, and later duplicate keys win.

intermediate5 min read
  • javascript
  • objects
  • polyfill
  • interview

Object.fromEntries(Object.entries(obj)) looks like a clone. It is a new ordinary object whose own enumerable string keys were Get then Set. Symbols disappear. Non-enumerable names disappear. The prototype is Object.prototype, not the original. Nested objects are the same references. Getters become data properties holding the value the getter returned. And fromEntries is not new Map: an object key stringifies, two pairs with the same string key collapse, last write wins.

Write standalone myEntries and myFromEntries. Patching Object mutates a shared builtin.

Key takeaways

  • entries is Object.keys plus Get, as [key, value] pairs in that order.
  • fromEntries takes any iterable of pairs, not just an array, and builds a fresh object.
  • Duplicate keys: the last pair wins. That is assignment, not a Map merge.
  • Round-trip drops symbols, hidden keys, and the original prototype. Nested values are not cloned.
  • Map keeps object keys and insertion of those keys. fromEntries runs ToPropertyKey.

Round-trip own enumerable string keys, not a Map

Object.entries is EnumerableOwnPropertyNames with kind key+value. Same key set and order as Object.keys. Each value is a Get, so getters run.

Object.fromEntries creates {}, iterates the input, and for each pair does Get(pair, "0"), Get(pair, "1"), ToPropertyKey, then CreateDataPropertyOrThrow. A non-object pair throws. Last duplicate key overwrites.

function myEntries(value) {
  if (value === null || value === undefined) {
    throw new TypeError('Cannot convert undefined or null to object');
  }
  const O = Object(value);
  const keys = [];
  for (const key in O) {
    if (Object.prototype.hasOwnProperty.call(O, key)) {
      keys.push(key);
    }
  }
  const integerKeys = [];
  const otherKeys = [];
  for (const key of keys) {
    const n = Number(key);
    const isIndex =
      n >= 0 && n < 0xffffffff && String(n) === key && Number.isInteger(n);
    if (isIndex) integerKeys.push(key);
    else otherKeys.push(key);
  }
  integerKeys.sort((a, b) => a - b);
  return integerKeys.concat(otherKeys).map((key) => [key, O[key]]);
}

function myFromEntries(iterable) {
  const obj = {};
  for (const pair of iterable) {
    if (pair === null || (typeof pair !== 'object' && typeof pair !== 'function')) {
      throw new TypeError('Iterator value is not an entry object');
    }
    obj[pair[0]] = pair[1];
  }
  return obj;
}

const source = { z: 1, 1: 'b', a: 2, 0: 'a' };
myEntries(source); // [['0', 'a'], ['1', 'b'], ['z', 1], ['a', 2]]

myFromEntries([
  ['a', 1],
  ['a', 2],
  ['b', 3],
]);
// { a: 2, b: 3 }  last 'a' wins

If you already have myKeys, myEntries is myKeys(O).map((key) => [key, O[key]]). Do not invent a second key order.

What the round-trip drops

const label = Symbol('label');
const proto = { inherited: 9 };
const original = Object.create(proto, {
  visible: { value: { n: 1 }, enumerable: true },
  quiet: { value: 2, enumerable: false },
  [label]: { value: 3, enumerable: true },
});

const copy = myFromEntries(myEntries(original));
copy.visible === original.visible;          // true, nested not cloned
copy.quiet;                                 // undefined
copy[label];                                // undefined
Object.getPrototypeOf(copy) === Object.prototype; // true
Feature Survives fromEntries(entries(obj)) Map
Enumerable string keys yes yes (as keys, any type)
Symbol keys no yes, if you set them
Non-enumerable strings no n/a (Map is not property flags)
Object as a key becomes '[object Object]' identity of that object
Duplicate string keys last wins last set wins, still one map key
Prototype reset to Object.prototype Map instance
Nested objects same reference same reference
const objKey = { id: 1 };
myFromEntries([
  [objKey, 'first'],
  [objKey, 'second'],
]);
// { '[object Object]': 'second' }

const map = new Map([
  [objKey, 'first'],
  [objKey, 'second'],
]);
map.get(objKey); // 'second', key is still objKey
map.size;        // 1

That is the “not a Map” line. fromEntries is a convenience for { k: v } records and for JSON-shaped data. It is the wrong tool when keys are objects, or when you need two keys that stringify the same.

new Map(Object.entries(obj)) is the conversion toward a Map. Keys in that Map are strings (what entries emitted). Object.fromEntries(map) is the conversion back, and it will stringify whatever keys the Map actually had. Neither direction is Array.prototype.entries, which yields numeric indexes: Object.entries(['a', 'b']) is [['0', 'a'], ['1', 'b']], strings, same as Object.keys on an array.

ToPropertyKey keeps actual symbols if you pass them in. myFromEntries([[Symbol.for('x'), 1]]) has a symbol key. Object.entries will not emit it, so a round-trip still drops symbols that started on a plain object. The loss is on the entries side.

Footguns in entries and fromEntries

Getters run during entries. A getter that throws aborts the list. After fromEntries, the property is a data field: the getter is gone.

myFromEntries(undefined) throws when you iterate. Empty iterable is {}. A pair that is null throws. A pair that is the primitive 'ab' also throws (TypeError: string is not an object). Only a boxed Object('ab') reads as 'a'/'b', which is legal and almost always a mistake.

Integer keys on the new object re-sort. fromEntries([['b', 1], ['1', 2], ['0', 3]]) yields keys ['0', '1', 'b'], not the pair order. Pair order only decides duplicate-key wins, not Object.keys order on the result.

Do not deep-clone here. Deep vs shallow copy is the follow-on. structuredClone is the platform clone; this API is a key/value rebuild.

Standalone first. Assigning Object.myEntries = myEntries mutates a builtin.

Interview answer (out loud)

“entries returns [key, value] for own enumerable string keys in Object.keys order, and Get runs so getters fire. fromEntries iterates pairs onto a new object; the last duplicate key wins. It is not a Map: object keys stringify, symbols from the original object never made it through entries, and the prototype is Object.prototype. Nested values are shared references. I would not call that a clone.”

Further reading

Related guides