ESC

Type to search the knowledge base.

Object.create Polyfill

Implement Object.create(proto, props): null prototype dictionaries, property descriptors, and why Object.create(undefined) is a TypeError.

beginner5 min read
  • 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

  • proto must be null or an object (functions count). Anything else is TypeError, including undefined.
  • Object.create(null) is a dictionary with no Object.prototype. Use Object.hasOwn, not obj.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

  1. If proto is not null and not an object, throw TypeError.
  2. Allocate {} and Object.setPrototypeOf(obj, proto).
  3. If props is not undefined, Object.defineProperties(obj, props).
  4. Return obj.
  5. Call out the constructor-trick null failure if they propose it.

Interview out-loud answer

Object.create builds a new object whose [[Prototype]] is the first argument. That argument must be an object or null; undefined is a TypeError. Object.create(null) is the dictionary with no Object.prototype. The optional second argument is property descriptors, same as defineProperties, and { value } defaults to non-writable. I would not use function F(){} for the null-proto case because new rewrites a null .prototype to Object.prototype.

Further reading

Related guides