Object.keys Polyfill
Polyfill Object.keys: own enumerable string keys in insertion order, no symbols, and integer-index keys listed before other strings.
- javascript
- objects
- polyfill
- interview
for...in is not Object.keys. for...in walks the prototype and, historically, did not promise order. Object.keys is own enumerable string keys only. Integer-index keys (canonical array indexes) come first, ascending numeric. Other strings follow in insertion order. Symbols never appear. A polyfill that returns Object.getOwnPropertyNames includes hidden keys. A polyfill that returns Reflect.ownKeys includes symbols too.
Write a standalone myKeys(value). Assigning Object.keys = myKeys is interview convention and mutates a shared builtin.
Key takeaways
- Own enumerable strings only. No inherited keys, no non-enumerable keys, no symbols.
- Array-index keys first in numeric order, then the rest in insertion order.
nullandundefinedthrow. Other primitives box:Object.keys('ab')is['0', '1'].getOwnPropertyNamesadds non-enumerable strings.Reflect.ownKeysadds symbols.for...inwithouthasOwncopies the prototype chain.
Enumerable string keys, insertion order, no symbols
EnumerableOwnPropertyNames filters [[OwnPropertyKeys]] to enumerable strings. OrdinaryOwnPropertyKeys is the order: array indexes ascending, remaining strings by creation time, then symbols (which keys drops).
function isArrayIndex(key) {
if (typeof key !== 'string') return false;
const n = Number(key);
return n >= 0 && n < 0xffffffff && String(n) === key && Number.isInteger(n);
}
function myKeys(value) {
if (value === null || value === undefined) {
throw new TypeError('Cannot convert undefined or null to object');
}
const O = Object(value);
const integerKeys = [];
const otherKeys = [];
for (const key in O) {
if (!Object.prototype.hasOwnProperty.call(O, key)) continue;
if (isArrayIndex(key)) integerKeys.push(key);
else otherKeys.push(key);
}
integerKeys.sort((a, b) => a - b);
return integerKeys.concat(otherKeys);
}
const sample = { z: 1, 2: 'c', a: 2, 0: 'a', 1: 'b' };
myKeys(sample); // ['0', '1', '2', 'z', 'a']
'2' was inserted before '0'. Numeric order still wins for array indexes. 'z' was inserted before 'a', and neither is an index, so insertion order stays. '01' is not canonical (String(Number('01')) is '1'), so it is a regular string and does not sort with 1. '-1' is not an array index.
hasOwnProperty must be borrowed. O.hasOwnProperty(key) fails if the object has its own hasOwnProperty or a null prototype (Object.create(null) has no methods). Object.prototype.hasOwnProperty.call is the usual fix. Object.hasOwn is the modern name.
What each enumerator actually returns
| API | Inherited | Non-enumerable strings | Symbols | Order |
|---|---|---|---|---|
Object.keys |
no | no | no | indexes, then insertion |
Object.getOwnPropertyNames |
no | yes | no | same string order |
Reflect.ownKeys |
no | yes | yes | strings, then symbols |
for...in |
yes (enumerable) | no | no | engine; filter with hasOwn |
const hidden = Symbol('hidden');
const proto = { inherited: 1 };
const obj = Object.create(proto, {
visible: { value: 2, enumerable: true },
quiet: { value: 3, enumerable: false },
[hidden]: { value: 4, enumerable: true },
});
obj.own = 5;
myKeys(obj); // ['visible', 'own'] (insertion)
Object.getOwnPropertyNames(obj); // includes 'quiet'
Reflect.ownKeys(obj); // includes hidden symbol
If the interviewer allows getOwnPropertyNames, the order-faithful body is: names, then keep those whose descriptor is enumerable. That still drops symbols. It is closer to the spec than for...in because it never consults the prototype.
function myKeysFromNames(value) {
if (value === null || value === undefined) {
throw new TypeError('Cannot convert undefined or null to object');
}
const O = Object(value);
return Object.getOwnPropertyNames(O).filter((key) => {
const desc = Object.getOwnPropertyDescriptor(O, key);
return desc && desc.enumerable;
});
}
getOwnPropertyNames already returns indexes first, then other strings. You do not sort again unless you built the list yourself.
Procedure
- Throw if the argument is
nullorundefined. Box everything else withObject(...). - Collect own enumerable string keys. Borrow
hasOwnif you are onfor...in. - Split array-index keys from the rest. Sort the indexes as numbers (
'10'after'2'). - Concatenate: indexes, then other strings in the order you first saw them.
- Leave symbols and non-enumerable names out. Do not walk the prototype.
Class instances follow the same rule. Own enumerable fields show up. Methods on the prototype do not. That is why Object.keys(new Map()) is [] and Object.keys({ size: 1 }) is ['size'].
Footguns in an Object.keys polyfill
Object.keys(null) and Object.keys(undefined) throw. Object.keys(3) is []. Object.keys('ab') is ['0', '1'] because a boxed String has enumerable index keys. Object.keys([, 'x']) is ['1']: the hole is not enumerable because it is not a property.
Getters are not run. keys returns names, not values. Values are entries.
Integer-index sort is numeric: '10' after '2'. Lexicographic sort (['10','2'].sort()) is the bug that prints '10' first.
Do not use Object.keys to implement myKeys. The for...in + hasOwn version is the no-helper walk. Mention that old engines’ for...in order was messy; the sort of array indexes is how you state the current spec out loud.
JSON.stringify walks enumerable string keys and also skips symbols. It omits object values that are undefined or functions. Object.keys still returns those keys: enumerability is about the property, not the value.
Patching Object.keys mutates a builtin. Object.assign is a consumer of this list (string keys; symbols are extra credit there).
Interview answer (out loud)
“Object.keys boxes the argument, throws on null and undefined, and returns own enumerable string keys. Array-index keys come first in numeric order, then other strings in insertion order. I skip symbols, inherited keys, and non-enumerable names. for-in needs a borrowed hasOwn check or it walks the prototype. getOwnPropertyNames is the same strings plus hidden ones. Reflect.ownKeys adds symbols.”
Further reading
Related on this site
- Object.assign polyfill
- Object.entries and fromEntries
- Object.create polyfill
- Symbols in JavaScript
- Proxy and Reflect
- 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.entries and fromEntriesRound-trip own enumerable string keys with Object.entries and Object.fromEntries. Not a Map, not symbols, and later duplicate keys win.
- 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.