Introspection
If you’ve spent much time with class oriented programming (either in JS or other languages), you’re probably familiar with type introspection: inspecting an instance to find out what kind of object it is. The primary goal of type introspection with class instances is to reason about the structure/capabilities of the object based on how it was created.
Consider this code which uses instanceof
(see Chapter 5) for introspecting on an object a1
to infer its capability:
function Foo() {
// ...
}
Foo.prototype.something = function(){
// ...
}
var a1 = new Foo();
// later
if (a1 instanceof Foo) {
a1.something();
}
Because Foo.prototype
(not Foo
!) is in the [[Prototype]]
chain (see Chapter 5) of a1
, the instanceof
operator (confusingly) pretends to tell us that a1
is an instance of the Foo
“class”. With this knowledge, we then assume that a1
has the capabilities described by the Foo
“class”.
Of course, there is no Foo
class, only a plain old normal function Foo
, which happens to have a reference to an arbitrary object (Foo.prototype
) that a1
happens to be delegation-linked to. By its syntax, instanceof
pretends to be inspecting the relationship between a1
and Foo
, but it’s actually telling us whether a1
and (the arbitrary object referenced by) Foo.prototype
are related.
The semantic confusion (and indirection) of instanceof
syntax means that to use instanceof
-based introspection to ask if object a1
is related to the capabilities object in question, you have to have a function that holds a reference to that object — you can’t just directly ask if the two objects are related.
Recall the abstract Foo
/ Bar
/ b1
example from earlier in this chapter, which we’ll abbreviate here:
function Foo() { /* .. */ }
Foo.prototype...
function Bar() { /* .. */ }
Bar.prototype = Object.create( Foo.prototype );
var b1 = new Bar( "b1" );
For type introspection purposes on the entities in that example, using instanceof
and .prototype
semantics, here are the various checks you might need to perform:
// relating `Foo` and `Bar` to each other
Bar.prototype instanceof Foo; // true
Object.getPrototypeOf( Bar.prototype ) === Foo.prototype; // true
Foo.prototype.isPrototypeOf( Bar.prototype ); // true
// relating `b1` to both `Foo` and `Bar`
b1 instanceof Foo; // true
b1 instanceof Bar; // true
Object.getPrototypeOf( b1 ) === Bar.prototype; // true
Foo.prototype.isPrototypeOf( b1 ); // true
Bar.prototype.isPrototypeOf( b1 ); // true
It’s fair to say that some of that kinda sucks. For instance, intuitively (with classes) you might want to be able to say something like Bar instanceof Foo
(because it’s easy to mix up what “instance” means to think it includes “inheritance”), but that’s not a sensible comparison in JS. You have to do Bar.prototype instanceof Foo
instead.
Another common, but perhaps less robust, pattern for type introspection, which many devs seem to prefer over instanceof
, is called “duck typing”. This term comes from the adage, “if it looks like a duck, and it quacks like a duck, it must be a duck”.
Example:
if (a1.something) {
a1.something();
}
Rather than inspecting for a relationship between a1
and an object that holds the delegatable something()
function, we assume that the test for a1.something
passing means a1
has the capability to call .something()
(regardless of if it found the method directly on a1
or delegated to some other object). In and of itself, that assumption isn’t so risky.
But “duck typing” is often extended to make other assumptions about the object’s capabilities besides what’s being tested, which of course introduces more risk (aka, brittle design) into the test.
One notable example of “duck typing” comes with ES6 Promises (which as an earlier note explained are not being covered in this book).
For various reasons, there’s a need to determine if any arbitrary object reference is a Promise, but the way that test is done is to check if the object happens to have a then()
function present on it. In other words, if any object happens to have a then()
method, ES6 Promises will assume unconditionally that the object is a “thenable” and therefore will expect it to behave conformantly to all standard behaviors of Promises.
If you have any non-Promise object that happens for whatever reason to have a then()
method on it, you are strongly advised to keep it far away from the ES6 Promise mechanism to avoid broken assumptions.
That example clearly illustrates the perils of “duck typing”. You should only use such approaches sparingly and in controlled conditions.
Turning our attention once again back to OLOO-style code as presented here in this chapter, type introspection turns out to be much cleaner. Let’s recall (and abbreviate) the Foo
/ Bar
/ b1
OLOO example from earlier in the chapter:
var Foo = { /* .. */ };
var Bar = Object.create( Foo );
Bar...
var b1 = Object.create( Bar );
Using this OLOO approach, where all we have are plain objects that are related via [[Prototype]]
delegation, here’s the quite simplified type introspection we might use:
// relating `Foo` and `Bar` to each other
Foo.isPrototypeOf( Bar ); // true
Object.getPrototypeOf( Bar ) === Foo; // true
// relating `b1` to both `Foo` and `Bar`
Foo.isPrototypeOf( b1 ); // true
Bar.isPrototypeOf( b1 ); // true
Object.getPrototypeOf( b1 ) === Bar; // true
We’re not using instanceof
anymore, because it’s confusingly pretending to have something to do with classes. Now, we just ask the (informally stated) question, “are you a prototype of me?” There’s no more indirection necessary with stuff like Foo.prototype
or the painfully verbose Foo.prototype.isPrototypeOf(..)
.
I think it’s fair to say these checks are significantly less complicated/confusing than the previous set of introspection checks. Yet again, we see that OLOO is simpler than (but with all the same power of) class-style coding in JavaScript.