JAVASCRIPT / PROTOTYPES, CLASSES, AND INHERITANCE
instanceof, isPrototypeOf, and type checks
Read instanceof and isPrototypeOf as prototype-chain identity tests, and choose typeof, Array.isArray, or toString brands when they mislead.
What you will learn
- Read `x instanceof C` as: is C.prototype anywhere in x's prototype chain?
- Use proto.isPrototypeOf(obj) when no constructor function owns the prototype.
- Pick typeof for primitives and Array.isArray for values from other realms.
- Recognise that Symbol.hasInstance can make instanceof disagree with the chain.
Understanding instanceof, isPrototypeOf, and type checks
`x instanceof C` does one thing: it reads `C.prototype`, then walks the chain of `x` — `Object.getPrototypeOf(x)`, that object's prototype, and so on — comparing each link to that one object by reference. It never asks which function was called with `new`, which is why an object built by `new Dog()` stops being `instanceof Dog` the moment you re-point its prototype. Two edge behaviours fall straight out of the mechanics: a primitive on the left has no chain to walk, so the answer is false immediately, and a right operand that is not callable produces a TypeError rather than false.
`isPrototypeOf` asks the same question without the middleman: `proto.isPrototypeOf(obj)` walks obj's chain looking for `proto` itself. So `Dog.prototype.isPrototypeOf(rex)` and `rex instanceof Dog` normally agree, with instanceof simply letting you name the prototype indirectly through the function that owns it. The two diverge when there is no such function, as with objects made by `Object.create`, and when a class defines `Symbol.hasInstance`, because instanceof calls that method and returns whatever it says while isPrototypeOf keeps reporting the real chain.
Since both checks compare object identity, they break at any boundary that duplicates prototypes. An iframe, a worker, a Node vm context, or the same package installed twice each build their own `Array.prototype` and their own class objects, so a genuine array from another realm fails `instanceof Array` while still behaving exactly like an array. That is why the platform ships `Array.isArray`: it inspects an internal marker carried by the value itself instead of a prototype link. For the same reason, prefer `typeof` for primitives and functions, `Object.prototype.toString.call(v)` for built-in brands, and a check for the method you actually intend to call when all you need is a capability.
Treat instanceof as a convenience over the chain walk, not as a record of how a value was born.
class Animal {}
class Dog extends Animal {}
const rex = new Dog();
console.log(typeof rex);
console.log(rex instanceof Dog, rex instanceof Animal, rex instanceof Object);
console.log(Dog.prototype.isPrototypeOf(rex));
// rex is still the object `new Dog()` returned, but instanceof only reads the chain
Object.setPrototypeOf(rex, Animal.prototype);
console.log(rex instanceof Dog, rex instanceof Animal);
console.log(Object.getPrototypeOf(rex) === Animal.prototype);instanceof and isPrototypeOf both answer one question — is this exact prototype object in that value's chain — so they test delegation identity, not what created the value.
Worked examples
Primitives are never instances
Shows why instanceof cannot validate a string and which check can.
const s = 'hello';
const boxed = new String('hello');
console.log(s instanceof String);
console.log(boxed instanceof String);
console.log(typeof s, typeof boxed);
console.log(Object.prototype.toString.call(s));Example explained
Line 1`s instanceof String` is false because instanceof rejects any non-object left operand before it inspects a chain.
Line 2`new String('hello')` builds a real object whose chain contains String.prototype, so that same check passes.
Line 3typeof is what separates the two cases: 'string' for the primitive, 'object' for the wrapper.
Line 4Object.prototype.toString boxes the primitive internally first, which is how it reports a String brand where instanceof could not.
Asking without a constructor
Demonstrates isPrototypeOf on plain and null-prototype objects, where instanceof throws or lies by omission.
const base = { greet() { return 'hi'; } };
const child = Object.create(base);
const bare = Object.create(null);
console.log(base.isPrototypeOf(child));
try {
child instanceof base;
} catch (err) {
console.log(err.constructor.name);
}
console.log(bare instanceof Object);
console.log(Object.prototype.isPrototypeOf.call(base, child));
console.log(typeof bare.isPrototypeOf);Example explained
Line 1base is a plain object with no .prototype property, so isPrototypeOf is the only tool that can answer the question at all.
Line 2instanceof throws when its right operand is not callable, so it cannot be used as a defensive check against arbitrary values.
Line 3bare came from Object.create(null) and has an empty chain, so even `bare instanceof Object` is false.
Line 4The .call form works on any target, including objects that inherit nothing and therefore own no isPrototypeOf method.
instanceof is a hook, not a fact
Overriding Symbol.hasInstance makes instanceof contradict both the prototype chain and the new keyword.
class Duck {
static [Symbol.hasInstance](value) {
return typeof value?.quack === 'function';
}
}
const decoy = { quack() { return 'quack'; } };
console.log(decoy instanceof Duck);
console.log(Object.getPrototypeOf(decoy) === Duck.prototype);
console.log(Duck.prototype.isPrototypeOf(decoy));
console.log(new Duck() instanceof Duck);Example explained
Line 1instanceof looks up Symbol.hasInstance on its right operand first, so it calls this static method instead of walking any chain.
Line 2The next two lines confirm decoy has no link to Duck.prototype, yet instanceof still reports true.
Line 3A genuine `new Duck()` fails the hook because Duck.prototype has no quack method, so instanceof can disagree with new itself.
Important notes
instanceof searches the entire chain, so `x instanceof Object` is true for almost every object; use `Object.getPrototypeOf(x) === C.prototype` when you want an exact match.
`Object.prototype.toString.call(v)` can be steered by a Symbol.toStringTag property, so treat its output as a strong convention rather than a guarantee.
Common mistakes
Writing `if (name instanceof String)` to validate text: it is false for every primitive, and the usual fix, `new String(name)`, creates an object that then fails `typeof name === 'string'` and `===` comparisons.
Calling isPrototypeOf with the arguments swapped, as in `obj.isPrototypeOf(Dog.prototype)`: it returns false rather than throwing, so the branch silently never runs and nothing shows up in the console.
Reassigning `Fn.prototype = { ... }` after instances already exist: those older objects still link to the discarded prototype, so `old instanceof Fn` becomes false while newly created ones stay true.
Try it yourself
Change, predict, then run
In a console, define `class A {}`, `class B extends A {}`, `const b = new B()`, then log `b instanceof A`, `b instanceof B`, `A.prototype.isPrototypeOf(b)` and `B.prototype.isPrototypeOf(b)`. Run `Object.setPrototypeOf(b, A.prototype)` and log the same four expressions again to see exactly which two flip.
Open the JavaScript workspaceCheck your understanding
An array built inside an iframe is handed to the parent page. There, `Array.isArray(arr)` is true but `arr instanceof Array` is false. What explains the difference?
- The iframe has its own Array constructor, so its arrays delegate to a different Array.prototype object than the one the parent's instanceof compares against.
- Crossing the frame boundary converts the array into a plain object, so it is no longer really an array.
- instanceof only works on values created with new, and array literals never touch the constructor.
- The parent's Array binding was shadowed, so instanceof ended up comparing against undefined.
Show answer
instanceof compares by reference against the parent realm's exact Array.prototype object, and the iframe's arrays link to the iframe's own Array.prototype, so the chain walk never finds a match; Array.isArray sidesteps this by reading an internal marker that travels with the value. The serialization option is tempting but wrong: the array is passed by reference and still works as an array, calling the iframe's own push and map through its own prototype.