Implement instanceof
Walk [[Prototype]] until you hit the constructor’s prototype. Symbol.hasInstance, primitives, and the null-prototype object that is nobody’s instance.
- 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.getPrototypeOfuntil you matchCtor.prototypeor hitnull.- If
Ctor[Symbol.hasInstance]is callable, that method wins (andFunction.prototypealready 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:
- Right-hand
Cmust be an object (elseTypeError). - Let
instOf = C[Symbol.hasInstance]. - If that is not
undefined, call it and returnToBoolean. - Else
Cmust 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
- If
Ctoris not an object, throw. - If
Ctor[Symbol.hasInstance]is defined, call it, return boolean. - If
Ctoris not callable, throw. - If the left-hand value is a primitive, return
false. - Read
Ctor.prototype. If it is not an object, throw. - Walk
getPrototypeOfuntil match ornull.
Interview out-loud answer
instanceofwalks[[Prototype]]looking forConstructor.prototype. Primitives arefalse.Object.create(null)never hitsObject.prototype, so it is not anObject. If@@hasInstanceexists I call that first; ordinary functions inherit one that also unwraps bound functions. A handmade walk willTypeErroronboundFn.prototypebeingundefined. Right-hand non-objects throw. I would not useinstanceof Arrayacross iframes.
Related on this site
- Prototypal Inheritance: the chain being walked
- Implement the new Operator: how
.prototypegets onto the instance - Object.create Polyfill:
Object.create(null)andObject.create(C.prototype) - Symbols in JavaScript: well-known
Symbol.hasInstance - call, apply, and bind: bound functions have no
.prototype - Type Guards typeof and instanceof: why TS still uses this operator
Further reading
Related guides
- Implement the new OperatorWrite myNew(Ctor, ...args): create object, set prototype, call constructor, return object unless the constructor returned an object.
- Array.filter PolyfillWrite filter that skips holes, respects thisArg, and never writes empty slots for rejected items. Contrast with map’s hole-preserving result.
- Array.find PolyfillWrite Array.prototype.find: first callback hit wins, holes are read as undefined, nothing matching returns undefined, and findIndex is the index twin.
- Array.flat PolyfillImplement flat(depth) with Infinity, holes, and non-array array-likes. Recursion vs stack, and why concat(spread) blows up on huge inputs.
- Array.forEach PolyfillImplement Array.prototype.forEach: visit present elements, skip holes, ignore the callback return, and honor thisArg without using native forEach.