In JavaScript, why would you build a string-keyed lookup table with `Object.create(null)` instead of `{}`, and what stops working on the resulting object?
answer
- an empty object is not empty
- inherited names collide with data keys
- chain ends immediately
- no toString means no string conversion
- Object.hasOwn instead of the method
basics
~20 sObject.create(null) makes an object with no prototype, so no key can collide with an inherited member such as toString or constructor and every property present is genuinely one you put there. The cost is that the object has no inherited methods at all.
solid answer
~40 sA plain `{}` inherits from `Object.prototype`, so it already answers to names like `toString`, `constructor`, `valueOf` and `hasOwnProperty`. If keys come from data, a key called `toString` makes a naive `if (map[key])` check return a function for something you never stored, and `key in map` is true for names nobody inserted. `Object.create(null)` gives you an object with a null prototype: the chain ends immediately, so every property it answers to is an own property you put there. The trade-off is that it inherits nothing — `dict.toString`, `dict.hasOwnProperty` and `dict.constructor` are all `undefined`, and string-converting it throws a TypeError because there is no `toString` to call. Static helpers still work: `Object.keys`, `Object.hasOwn`, `in`, `delete` and `JSON.stringify` are all fine.
code
javascript · 17 linesconst dict = Object.create(null);
dict.toString = 'user value';
console.log(Object.keys(dict)); // [ 'toString' ]
console.log('toString' in dict); // true - because we stored it
console.log(dict.constructor); // undefined
console.log(JSON.stringify(dict)); // {"toString":"user value"}
const plain = {};
console.log('toString' in plain); // true - inherited, nothing stored
console.log(typeof plain.toString); // function
try {
String(Object.create(null));
} catch (err) {
console.log(err.name); // TypeError
}go deeper
Know that {} already inherits names like toString and constructor, and that Object.create(null) gives you an object that inherits nothing at all.
Explain the mechanics both ways: which lookups the missing chain changes, why string conversion now throws, and which static helpers such as Object.keys and Object.hasOwn are unaffected.
Show the production judgment — where data-derived keys enter the system, why a bare if (map[key]) is the bug, and how you contain a null-prototype object so it never reaches code that string-converts or type-sniffs it.
Own the API decision: when a component should expose a plain JSON-shaped object versus a purpose-built keyed collection, and how you keep that choice consistent across module boundaries and serialization points.
## Why a plain object is a bad dictionary An object literal is not empty. `{}` is created with `Object.prototype` as its prototype, and that object carries `toString`, `valueOf`, `hasOwnProperty`, `isPrototypeOf`, `propertyIsEnumerable`, `constructor` and the legacy `__proto__` accessor. None of them are own properties, so `Object.keys({})` is `[]` — but property *lookup* walks the chain, so: ```js const counts = {}; counts['toString']; // [Function: toString], not undefined 'constructor' in counts; // true counts['valueOf'] ? 'seen' : 'new'; // 'seen', wrongly ``` When the keys are user data — form field names, header names, word-frequency tallies, cache keys — those inherited names are landmines. The classic bug is a presence test written as `if (map[key])` or `if (key in map)`: for a handful of magic strings it reports a hit that was never inserted, and downstream code then treats a built-in function as if it were your stored value. ## What Object.create(null) changes `Object.create(null)` is the standard way to build an object whose prototype chain is empty from the start. It has no inherited properties at all, so the lookup for any missing key ends immediately with `undefined`. ```js const dict = Object.create(null); dict.toString; // undefined 'constructor' in dict; // false dict.a = 1; Object.keys(dict); // [ 'a' ] ``` That restores the property you actually wanted: *present* means *inserted*. It also removes the special behaviour of the key `__proto__`, which on a normal object is an inherited accessor that reassigns the prototype rather than storing a value — on a null-prototype object it is just another string key. ## What you give up Everything reached through the prototype disappears: - `dict.hasOwnProperty(k)` throws a TypeError, because the method is not there. Use `Object.hasOwn(dict, k)` (ES2022) or `Object.prototype.hasOwnProperty.call(dict, k)`. - `dict.toString()` throws, and so does any implicit string conversion: `String(dict)`, `` `${dict}` `` and `'x' + dict` all raise `TypeError: Cannot convert object to primitive value`, because neither `toString` nor `valueOf` exists for the ToPrimitive step to call. - `dict instanceof Object` is `false`, and `dict.constructor` is `undefined`, so code that sniffs objects that way will misclassify it. What still works is anything that does not go through the chain: `Object.keys` / `values` / `entries`, `Object.assign`, `in`, `delete`, `for...in` (which now yields only own keys), spread, and `JSON.stringify`. Node's `console.log` prints such objects with a `[Object: null prototype]` marker, which is a useful signal in logs rather than a problem. ```js const dict = Object.create(null); dict.b = 2; JSON.stringify(dict); // '{"b":2}' Object.hasOwn(dict, 'b'); // true ``` ## Where it fits, and the alternative Use a null-prototype object when you need a dictionary keyed by untrusted or data-derived strings and you also want it to remain a plain JSON-shaped object — something you will `JSON.stringify`, pass to code expecting an object literal, or return from a parser. Parsers and header bags in real libraries do exactly this. The language also ships a purpose-built container for key/value work whose keys are never property names at all, and in new code that is often the better default; a null-prototype object is the right answer specifically when the result must still *be* an object with string properties. A middle ground that avoids the missing-method cost is to keep `{}` and simply never trust bare lookups: guard every read with `Object.hasOwn(map, key)`. That is correct and portable, but it relies on discipline at every call site, whereas `Object.create(null)` makes the whole class of collision impossible once, at creation. One practical caution: a null-prototype object leaks its strangeness into anything that touches it. Template literals, string concatenation in a log line, and libraries that call `value.toString()` or check `instanceof Object` will behave unexpectedly. Keep such objects internal to the code that understands them, and convert with `Object.assign({}, dict)` at the boundary if the consumer expects an ordinary object.
- If you keep using `{}`, how do you make presence checks safe?Never test with a bare read. Use `Object.hasOwn(map, key)` in ES2022 and later, or `Object.prototype.hasOwnProperty.call(map, key)` before that — calling the method through `map.hasOwnProperty` is itself unsafe, since a data key of that name shadows it. The check is correct but must be applied at every call site.
- Why does `` `${dict}` `` throw for a null-prototype object?A template literal converts the value with ToString, which for an object runs ToPrimitive. That step looks for `toString` and then `valueOf` on the object and its chain; a null-prototype object has neither, so there is nothing to call and the engine throws `TypeError: Cannot convert object to primitive value`. Adding an own `toString` function fixes it.
- How do you hand a null-prototype object to a library that expects an ordinary object?Copy it at the boundary with `Object.assign({}, dict)` or `{ ...dict }`. Both produce a fresh object whose prototype is `Object.prototype` while carrying the same own enumerable keys, so the consumer gets `toString`, `instanceof Object` and anything else it might rely on.
saying these in an interview costs you the question
- Says an object literal starts out with no properties at all
- Calls dict.hasOwnProperty on a null-prototype object
- Thinks Object.keys({}) being empty makes lookups safe
- Claims Object.create(null) blocks adding new keys
- Believes JSON.stringify fails on a null-prototype object