ESC

Type to search the knowledge base.

Implement instanceof

Walk [[Prototype]] until you hit the constructor’s prototype. Symbol.hasInstance, primitives, and the null-prototype object that is nobody’s instance.

intermediate5 min read
  • javascript
  • prototypes
  • polyfill
  • interview

x instanceof C is not “x was built by C.” It is “C.prototype appears on x’s [[Prototype]] chain,” unless C customized Symbol.hasInstance. That is why Object.create(Person.prototype) instanceof Person is true with no new, and why Object.create(null) instanceof Object is false.

Primitives are never instances. 'hi' instanceof String is false; new String('hi') instanceof String is true. The right-hand side must be an object, and if you fall through to the ordinary walk its .prototype must be an object or you throw TypeError.

Key takeaways

  • Ordinary path: walk Object.getPrototypeOf until you match Ctor.prototype or hit null.
  • If Ctor[Symbol.hasInstance] is callable, that method wins (and Function.prototype already provides one).
  • Left-hand primitives → false. Right-hand non-object → TypeError. Non-object .prototype → TypeError.
  • Bound functions have no .prototype. The real operator unwraps [[BoundTargetFunction]]. Object.create(null) is nobody’s instance.

Walk the chain, then honor Symbol.hasInstance

OrdinaryHasInstance, the walk you should be able to write from memory:

function ordinaryHasInstance(Ctor, value) {
  if (typeof Ctor !== 'function') return false;
  if (value === null || (typeof value !== 'object' && typeof value !== 'function')) {
    return false; // primitives, including null and undefined
  }

  const proto = Ctor.prototype;
  if (proto === null || (typeof proto !== 'object' && typeof proto !== 'function')) {
    throw new TypeError('Function has non-object prototype in instanceof check');
  }

  let current = Object.getPrototypeOf(value);
  while (current !== null) {
    if (current === proto) return true;
    current = Object.getPrototypeOf(current);
  }
  return false;
}
function Animal() {}
function Dog() {}
Dog.prototype = Object.create(Animal.prototype);

const d = new Dog();
ordinaryHasInstance(Dog, d);     // true
ordinaryHasInstance(Animal, d);  // true
ordinaryHasInstance(Array, d);   // false
ordinaryHasInstance(Dog, {});    // false
ordinaryHasInstance(String, 'x'); // false

Object.create(null) has no chain:

const dict = Object.create(null);
ordinaryHasInstance(Object, dict); // false
dict instanceof Object;            // false

It is a dictionary, not an Object instance. Same reason dict.toString is undefined. See Object.create and the prototype chain.

Left-hand value Typical result vs Object / String
{} / [] / function f(){} true for Object (chain hits Object.prototype)
'hi' / 1 / true / null / undefined false (not an object; walk never starts)
new String('hi') true for String and Object
Object.create(null) false for Object (chain is null)
Object.create(C.prototype) true for C even though new C never ran

Reassigning C.prototype after instances exist changes later instanceof checks. The operator reads the current .prototype, not the one used at construct time.

Symbol.hasInstance first

The actual operator is not “always walk.” It is:

  1. Right-hand C must be an object (else TypeError).
  2. Let instOf = C[Symbol.hasInstance].
  3. If that is not undefined, call it and return ToBoolean.
  4. Else C must be callable, then OrdinaryHasInstance.

Every ordinary function inherits Function.prototype[Symbol.hasInstance], which is OrdinaryHasInstance (plus bound-function unwrapping). A complete helper therefore tries @@hasInstance first:

function myInstanceof(value, Ctor) {
  if (Ctor === null || (typeof Ctor !== 'object' && typeof Ctor !== 'function')) {
    throw new TypeError('Right-hand side of instanceof is not an object');
  }

  const hasInstance = Ctor[Symbol.hasInstance];
  if (hasInstance !== undefined) {
    if (typeof hasInstance !== 'function') {
      throw new TypeError('@@hasInstance is not a function');
    }
    return Boolean(hasInstance.call(Ctor, value));
  }

  if (typeof Ctor !== 'function') {
    throw new TypeError('Right-hand side of instanceof is not callable');
  }
  return ordinaryHasInstance(Ctor, value);
}

Customizing the operator:

class Even {
  static [Symbol.hasInstance](value) {
    return Number.isInteger(value) && value % 2 === 0;
  }
}

2 instanceof Even; // true
3 instanceof Even; // false

If you only walk .prototype, you miss this. If you only call Ctor[Symbol.hasInstance] and never write the walk, you have not shown OrdinaryHasInstance. Do both: walk as the engine’s default, @@hasInstance as the override.

Deleting the inherited method is exotic. The hasInstance !== undefined branch then falls through to your walk. Worth one sentence, not a rabbit hole.

Bound functions (footnote)

function C() {}
const Bound = C.bind(null);

Bound.prototype;          // undefined
new C() instanceof Bound; // true  (engine)
ordinaryHasInstance(Bound, new C()); // TypeError: non-object prototype

Bound functions do not have a prototype property. OrdinaryHasInstance in the spec checks [[BoundTargetFunction]] and restarts on the target, so instanceof Bound uses C.prototype. User code cannot read that internal slot.

Practical polyfill: let Function.prototype[Symbol.hasInstance] run (the first branch above). A whiteboard-only walk will throw or return false for bound constructors. Say that. Do not parse Bound.name.

new Bound() still works; Implement the new Operator has the matching footnote.

Precedence: !x instanceof C is a bug

instanceof binds less tightly than unary !. !x instanceof C is (!x) instanceof C, almost always false (a boolean is a primitive). Write !(x instanceof C).

Realms, brands, private fields

[] instanceof iframe.contentWindow.Array is false. Different realm, different Array.prototype. Array.isArray([]) stays true across frames. If the question is “is this an array?”, instanceof Array is the wrong tool.

instanceof is also not a brand check for class private fields. Object.create(C.prototype) instanceof C is true, then C.#field access still throws. @@hasInstance can be overridden to do a real brand check (#field in obj). That is a follow-up, not the default implementation.

Procedure

  1. If Ctor is not an object, throw.
  2. If Ctor[Symbol.hasInstance] is defined, call it, return boolean.
  3. If Ctor is not callable, throw.
  4. If the left-hand value is a primitive, return false.
  5. Read Ctor.prototype. If it is not an object, throw.
  6. Walk getPrototypeOf until match or null.

Interview out-loud answer

instanceof walks [[Prototype]] looking for Constructor.prototype. Primitives are false. Object.create(null) never hits Object.prototype, so it is not an Object. If @@hasInstance exists I call that first; ordinary functions inherit one that also unwraps bound functions. A handmade walk will TypeError on boundFn.prototype being undefined. Right-hand non-objects throw. I would not use instanceof Array across iframes.

Further reading

Related guides