ESC

Type to search the knowledge base.

Implement the new Operator

Write myNew(Ctor, ...args): create object, set prototype, call constructor, return object unless the constructor returned an object.

intermediate5 min read
  • javascript
  • prototypes
  • polyfill
  • interview

Most constructors never return. A few do, on purpose: a connection pool hands back a cached client, Error subclasses sometimes don’t, factories hide the instance. If your myNew always returns the object you allocated, you missed the last step of [[Construct]]. If you treat typeof result === 'object' as the test and forget that typeof null === 'object', you throw the instance away when the constructor returns null.

new is not “call the function.” It is allocate, link [[Prototype]], invoke with that object as this, then maybe discard the object because the constructor returned a different object (including a function).

Key takeaways

  • Object.create(Ctor.prototype) (with a fallback when .prototype is not an object), then Ctor.apply(obj, args).
  • If the constructor returns an object or a function, that value is the result. Primitives, undefined, and null are ignored.
  • ES6 class constructors throw when called. apply cannot fake new for them. Reflect.construct can.
  • Bound functions: new boundFn() uses the original function’s .prototype.

Object.create, call, then maybe throw the instance away

The operator does four jobs (MDN new):

  1. Create a blank object.
  2. Set its [[Prototype]] to Ctor.prototype when that value is an object. Otherwise use Object.prototype.
  3. Run Ctor with this bound to the new object, forwarding arguments.
  4. If the run returns a non-primitive, return that. Else return the allocated object.
function myNew(Ctor, ...args) {
  if (typeof Ctor !== 'function') {
    throw new TypeError(`${Ctor} is not a constructor`);
  }

  const proto = Ctor.prototype;
  const obj = Object.create(
    proto !== null && (typeof proto === 'object' || typeof proto === 'function')
      ? proto
      : Object.prototype,
  );

  const result = Ctor.apply(obj, args);

  if (result !== null && (typeof result === 'object' || typeof result === 'function')) {
    return result;
  }
  return obj;
}
function Person(name) {
  this.name = name;
}
Person.prototype.greet = function greet() {
  return `hi ${this.name}`;
};

const p = myNew(Person, 'Ada');
p.greet();                         // 'hi Ada'
Object.getPrototypeOf(p) === Person.prototype; // true
p instanceof Person;               // true

The instanceof check works because we wired [[Prototype]] to Person.prototype. Same model as Prototypal Inheritance.

Return override

function Cache() {
  this.fresh = true;
  return { cached: true }; // object → wins
}
myNew(Cache).cached; // true
myNew(Cache) instanceof Cache; // false

function Nullish() {
  this.ok = true;
  return null; // ignored (`typeof null === 'object'` is the trap)
}
myNew(Nullish).ok; // true

function Boxed() {
  return 42; // primitive → ignored
}
myNew(Boxed) instanceof Boxed; // true

function Factory() {
  return function inner() { return 1; }; // functions are objects
}
typeof myNew(Factory); // 'function'

The null case is the one people fail. Spec Type for Null is not Object. User code must special-case result !== null before the typeof test. Functions must pass: typeof fn === 'function', and they are valid construct results.

.prototype that is not an object

function Broken() {
  this.x = 1;
}
Broken.prototype = 3; // or null

const b = myNew(Broken);
Object.getPrototypeOf(b) === Object.prototype; // true
b instanceof Broken; // TypeError: prototype is not an object

new still succeeds. instanceof then throws because the right-hand .prototype is not an object. Handle that in Object.create’s argument, not by assuming every function has a usable .prototype.

What myNew cannot do

Case new Ctor() myNew(Ctor)
Ordinary function constructs constructs
class C {} constructs TypeError (class must be constructed, apply calls)
Arrow function TypeError (no [[Construct]]) TypeError or a thrown body, no instance
new.target inside Ctor the constructor undefined (apply is a call)
Bound function uses target’s .prototype uses boundFn.prototype which is undefined → Object.prototype

The honest follow-up: Reflect.construct(Ctor, args) is the platform equivalent. It sets new.target, constructs classes, and unwraps bound functions.

function myNewReflect(Ctor, ...args) {
  return Reflect.construct(Ctor, args);
}

Whiteboard myNew is still worth writing. It proves you know the four steps. Then you name the class / new.target gap.

Bound functions (footnote)

function C(x) {
  this.x = x;
}
const Bound = C.bind(null, 1);
const a = new Bound();
a.x; // 1
a instanceof C; // true
a instanceof Bound; // true (engine unwraps [[BoundTargetFunction]])

Bound.prototype is undefined. A naive Object.create(Bound.prototype) becomes Object.create(Object.prototype) in the fallback, so a instanceof C fails. To match new, either call Reflect.construct or detect bound functions (Bound.name starting with "bound " is not a spec check; don’t). Say you would defer to Reflect.construct for this case.

this inside the constructor

Ctor.apply(obj, args) is the explicit-binding rule. Arrow constructors do not exist in any useful sense: arrows have no [[Construct]] and lexical this. If someone passes an arrow, throwing is correct.

Do not use new Ctor(...args) inside myNew. That cheats the prompt.

Procedure

  1. Reject non-functions.
  2. proto = Ctor.prototype. If it is not an object, use Object.prototype.
  3. obj = Object.create(proto).
  4. result = Ctor.apply(obj, args).
  5. Return result when it is a non-null object or a function. Otherwise return obj.
  6. Name classes, new.target, and bound functions as Reflect.construct territory.

Interview out-loud answer

myNew allocates with Object.create(Ctor.prototype), calls the function with that object as this, and returns the constructor’s return value when it is an object or a function. null and primitives do not override, which is why the typeof test has to exclude null. Class constructors cannot be apply’d, so this helper is a model of ordinary functions. Bound functions and new.target need Reflect.construct. The prototype link is the same one instanceof walks.

Further reading

Related guides