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:

  1. function Foo() {
  2. // ...
  3. }
  4. Foo.prototype.something = function(){
  5. // ...
  6. }
  7. var a1 = new Foo();
  8. // later
  9. if (a1 instanceof Foo) {
  10. a1.something();
  11. }

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:

  1. function Foo() { /* .. */ }
  2. Foo.prototype...
  3. function Bar() { /* .. */ }
  4. Bar.prototype = Object.create( Foo.prototype );
  5. 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:

  1. // relating `Foo` and `Bar` to each other
  2. Bar.prototype instanceof Foo; // true
  3. Object.getPrototypeOf( Bar.prototype ) === Foo.prototype; // true
  4. Foo.prototype.isPrototypeOf( Bar.prototype ); // true
  5. // relating `b1` to both `Foo` and `Bar`
  6. b1 instanceof Foo; // true
  7. b1 instanceof Bar; // true
  8. Object.getPrototypeOf( b1 ) === Bar.prototype; // true
  9. Foo.prototype.isPrototypeOf( b1 ); // true
  10. 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:

  1. if (a1.something) {
  2. a1.something();
  3. }

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:

  1. var Foo = { /* .. */ };
  2. var Bar = Object.create( Foo );
  3. Bar...
  4. 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:

  1. // relating `Foo` and `Bar` to each other
  2. Foo.isPrototypeOf( Bar ); // true
  3. Object.getPrototypeOf( Bar ) === Foo; // true
  4. // relating `b1` to both `Foo` and `Bar`
  5. Foo.isPrototypeOf( b1 ); // true
  6. Bar.isPrototypeOf( b1 ); // true
  7. 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.