Object.create Polyfill
Implement Object.create(proto, props): null prototype dictionaries, property descriptors, and why Object.create(undefined) is a TypeError.
- javascript
- objects
- polyfill
- prototypes
{} always delegates to Object.prototype. That is why a dictionary keyed by user strings can pick up toString, constructor, or a polluted __proto__. Object.create(proto) is how you choose the [[Prototype]] on purpose: a parent object, or null for a blank map.
Object.create(undefined) is not “also blank.” undefined is not an object and not null. The spec throws TypeError. Same for numbers, booleans, strings. The empty-constructor trick people copy from old blogs also fails for null: new F() with F.prototype = null still ends up with Object.prototype.
Key takeaways
protomust benullor an object (functions count). Anything else isTypeError, includingundefined.Object.create(null)is a dictionary with noObject.prototype. UseObject.hasOwn, notobj.hasOwnProperty.- Second argument is a descriptor map, same shape as
Object.defineProperties, not a bag of values.- The
function F(){}; F.prototype = proto; new F()trick cannot represent a null prototype.
A blank object whose [[Prototype]] you chose
function objectCreate(proto, props) {
if (proto !== null && typeof proto !== 'object' && typeof proto !== 'function') {
throw new TypeError('Object prototype may only be an Object or null');
}
const obj = {};
Object.setPrototypeOf(obj, proto);
if (props !== undefined) {
Object.defineProperties(obj, props);
}
return obj;
}
const animal = {
eat() {
return `${this.name} eats`;
},
};
const dog = objectCreate(animal);
dog.name = 'Rex';
dog.eat(); // 'Rex eats'
Object.getPrototypeOf(dog) === animal; // true
Own properties sit on dog. Methods live on animal. Lookup walks the link. That is prototypal inheritance, not a class copy.
Null vs undefined vs Object.prototype
| Call | [[Prototype]] |
Notes |
|---|---|---|
Object.create(null) |
null |
Safe dictionary. No toString, no constructor. |
Object.create({}) |
that object | Fine, but {} itself still has Object.prototype further up. |
Object.create(Object.prototype) |
Object.prototype |
Same as {}. |
Object.create(undefined) |
throws | Not an object, not null. |
Object.create(1) |
throws | Same rule. |
Object.create(Math.abs) |
Math.abs |
Functions are objects. Rare, legal. |
const map = objectCreate(null);
map.user = 1;
map.toString; // undefined
map instanceof Object; // false
// Object.create(undefined); // TypeError
If you are implementing a polyfill of Object.create, this throw is the test. Interviewers pass undefined on purpose.
Because there is no hasOwnProperty method on a null-proto object, write Object.hasOwn(map, key) (or Object.prototype.hasOwnProperty.call).
Second argument: descriptors, not values
const point = objectCreate(Object.prototype, {
x: { value: 0, writable: true, enumerable: true, configurable: true },
y: { value: 0, writable: true, enumerable: true, configurable: true },
id: { value: 'p', enumerable: false },
});
Object.keys(point); // ['x', 'y'] id is non-enumerable
Default descriptor flags for { value: 42 } are all false: not writable, not enumerable, not configurable. That shocks people who thought the second arg was { p: 42 }.
props === undefined skips the define step. props === null should throw (defineProperties does). Do not coerce null to “no props.”
A minimal sketch if they ban defineProperties:
function definePropertiesSketch(obj, props) {
for (const key of Object.keys(props)) {
const desc = props[key];
if (desc !== null && typeof desc === 'object') {
Object.defineProperty(obj, key, desc);
} else {
throw new TypeError('Property description must be an object');
}
}
// extra credit: enumerable own symbols on `props`
}
Getters and setters in the descriptor (get, set) must be functions. Mixing get with value throws. You do not need to reimplement that if you delegate to defineProperty.
Why the constructor trick fails for null
Old polyfills:
function objectCreateLegacy(proto) {
function F() {}
F.prototype = proto;
return new F();
}
When proto is an object, this works: new F sets [[Prototype]] to F.prototype. When proto is null, the new operator sees a non-object .prototype and falls back to Object.prototype. You silently did not create a dictionary.
function F() {}
F.prototype = null;
Object.getPrototypeOf(new F()) === Object.prototype; // true
So a polyfill that claims to support Object.create(null) needs Object.setPrototypeOf, __proto__ assignment, or a host hook. In an interview, write setPrototypeOf and mention the trick’s null bug. If setPrototypeOf is also off-limits, say the null case is impossible in pure ES3.
Patching Object.create itself is interview convention and mutates a shared builtin. Prefer the standalone function in real code.
new vs Object.create
Object.create(Ctor.prototype) allocates the instance new would allocate, without running Ctor. Implement the new Operator is “create, then apply.” This page is only the create.
Object.create(C.prototype) instanceof C is true even though C never ran. Private fields and initialization side effects are missing. Do not use it as a substitute for new unless that is the point.
Procedure
- If
protois notnulland not an object, throwTypeError. - Allocate
{}andObject.setPrototypeOf(obj, proto). - If
propsis notundefined,Object.defineProperties(obj, props). - Return
obj. - Call out the constructor-trick
nullfailure if they propose it.
Interview out-loud answer
Object.createbuilds a new object whose[[Prototype]]is the first argument. That argument must be an object ornull;undefinedis aTypeError.Object.create(null)is the dictionary with noObject.prototype. The optional second argument is property descriptors, same asdefineProperties, and{ value }defaults to non-writable. I would not usefunction F(){}for the null-proto case becausenewrewrites a null.prototypetoObject.prototype.
Related on this site
- Object.create and the prototype chain: own vs inherited, dictionaries
- Prototypal Inheritance: what the link is for
- Implement instanceof:
Object.create(null) instanceof Objectis false - Implement the new Operator: create plus constructor call
- Object.assign Polyfill: copying onto a target you may have just created
- Getters and Setters: descriptor
get/set
Further reading
Related guides
- Implement instanceofWalk [[Prototype]] until you hit the constructor’s prototype. Symbol.hasInstance, primitives, and the null-prototype object that is nobody’s instance.
- Implement the new OperatorWrite myNew(Ctor, ...args): create object, set prototype, call constructor, return object unless the constructor returned an object.
- 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.
- Object.keys PolyfillPolyfill Object.keys: own enumerable string keys in insertion order, no symbols, and integer-index keys listed before other strings.