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.
- 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
entriesis Object.keys plusGet, as[key, value]pairs in that order.fromEntriestakes 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.
fromEntriesrunsToPropertyKey.
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 on this site
- Object.keys polyfill
- Object.assign polyfill
- Map and Set
- Deep vs shallow copy
- Symbols in JavaScript
- Polyfill questions playbook
- JavaScript interview learning path
Related guides
- Object.assign PolyfillCopy enumerable own properties onto a target. Getters that run, symbols, null/undefined sources skipped, and the mutation-in-place return value.
- Object.keys PolyfillPolyfill Object.keys: own enumerable string keys in insertion order, no symbols, and integer-index keys listed before other strings.
- Array.filter PolyfillWrite filter that skips holes, respects thisArg, and never writes empty slots for rejected items. Contrast with map’s hole-preserving result.
- Array.find PolyfillWrite Array.prototype.find: first callback hit wins, holes are read as undefined, nothing matching returns undefined, and findIndex is the index twin.
- Array.flat PolyfillImplement flat(depth) with Infinity, holes, and non-array array-likes. Recursion vs stack, and why concat(spread) blows up on huge inputs.