“Class”

At this point, you might be wondering: “Why does one object need to link to another object?” What’s the real benefit? That is a very appropriate question to ask, but we must first understand what [[Prototype]] is not before we can fully understand and appreciate what it is and how it’s useful.

As we explained in Chapter 4, in JavaScript, there are no abstract patterns/blueprints for objects called “classes” as there are in class-oriented languages. JavaScript just has objects.

In fact, JavaScript is almost unique among languages as perhaps the only language with the right to use the label “object oriented”, because it’s one of a very short list of languages where an object can be created directly, without a class at all.

In JavaScript, classes can’t (being that they don’t exist!) describe what an object can do. The object defines its own behavior directly. There’s just the object.

“Class” Functions

There’s a peculiar kind of behavior in JavaScript that has been shamelessly abused for years to hack something that looks like “classes”. We’ll examine this approach in detail.

The peculiar “sort-of class” behavior hinges on a strange characteristic of functions: all functions by default get a public, non-enumerable (see Chapter 3) property on them called prototype, which points at an otherwise arbitrary object.

  1. function Foo() {
  2. // ...
  3. }
  4. Foo.prototype; // { }

This object is often called “Foo’s prototype”, because we access it via an unfortunately-named Foo.prototype property reference. However, that terminology is hopelessly destined to lead us into confusion, as we’ll see shortly. Instead, I will call it “the object formerly known as Foo’s prototype”. Just kidding. How about: “object arbitrarily labeled ‘Foo dot prototype’”?

Whatever we call it, what exactly is this object?

The most direct way to explain it is that each object created from calling new Foo() (see Chapter 2) will end up (somewhat arbitrarily) [[Prototype]]-linked to this “Foo dot prototype” object.

Let’s illustrate:

  1. function Foo() {
  2. // ...
  3. }
  4. var a = new Foo();
  5. Object.getPrototypeOf( a ) === Foo.prototype; // true

When a is created by calling new Foo(), one of the things (see Chapter 2 for all four steps) that happens is that a gets an internal [[Prototype]] link to the object that Foo.prototype is pointing at.

Stop for a moment and ponder the implications of that statement.

In class-oriented languages, multiple copies (aka, “instances”) of a class can be made, like stamping something out from a mold. As we saw in Chapter 4, this happens because the process of instantiating (or inheriting from) a class means, “copy the behavior plan from that class into a physical object”, and this is done again for each new instance.

But in JavaScript, there are no such copy-actions performed. You don’t create multiple instances of a class. You can create multiple objects that [[Prototype]] link to a common object. But by default, no copying occurs, and thus these objects don’t end up totally separate and disconnected from each other, but rather, quite linked.

new Foo() results in a new object (we called it a), and that new object a is internally [[Prototype]] linked to the Foo.prototype object.

We end up with two objects, linked to each other. That’s it. We didn’t instantiate a class. We certainly didn’t do any copying of behavior from a “class” into a concrete object. We just caused two objects to be linked to each other.

In fact, the secret, which eludes most JS developers, is that the new Foo() function calling had really almost nothing direct to do with the process of creating the link. It was sort of an accidental side-effect. new Foo() is an indirect, round-about way to end up with what we want: a new object linked to another object.

Can we get what we want in a more direct way? Yes! The hero is Object.create(..). But we’ll get to that in a little bit.

What’s in a name?

In JavaScript, we don’t make copies from one object (“class”) to another (“instance”). We make links between objects. For the [[Prototype]] mechanism, visually, the arrows move from right to left, and from bottom to top.

Class - 图1

This mechanism is often called “prototypal inheritance” (we’ll explore the code in detail shortly), which is commonly said to be the dynamic-language version of “classical inheritance”. It’s an attempt to piggy-back on the common understanding of what “inheritance” means in the class-oriented world, but tweak (read: pave over) the understood semantics, to fit dynamic scripting.

The word “inheritance” has a very strong meaning (see Chapter 4), with plenty of mental precedent. Merely adding “prototypal” in front to distinguish the actually nearly opposite behavior in JavaScript has left in its wake nearly two decades of miry confusion.

I like to say that sticking “prototypal” in front of “inheritance” to drastically reverse its actual meaning is like holding an orange in one hand, an apple in the other, and insisting on calling the apple a “red orange”. No matter what confusing label I put in front of it, that doesn’t change the fact that one fruit is an apple and the other is an orange.

The better approach is to plainly call an apple an apple — to use the most accurate and direct terminology. That makes it easier to understand both their similarities and their many differences, because we all have a simple, shared understanding of what “apple” means.

Because of the confusion and conflation of terms, I believe the label “prototypal inheritance” itself (and trying to mis-apply all its associated class-orientation terminology, like “class”, “constructor”, “instance”, “polymorphism”, etc) has done more harm than good in explaining how JavaScript’s mechanism really works.

“Inheritance” implies a copy operation, and JavaScript doesn’t copy object properties (natively, by default). Instead, JS creates a link between two objects, where one object can essentially delegate property/function access to another object. “Delegation” (see Chapter 6) is a much more accurate term for JavaScript’s object-linking mechanism.

Another term which is sometimes thrown around in JavaScript is “differential inheritance”. The idea here is that we describe an object’s behavior in terms of what is different from a more general descriptor. For example, you explain that a car is a kind of vehicle, but one that has exactly 4 wheels, rather than re-describing all the specifics of what makes up a general vehicle (engine, etc).

If you try to think of any given object in JS as the sum total of all behavior that is available via delegation, and in your mind you flatten all that behavior into one tangible thing, then you can (sorta) see how “differential inheritance” might fit.

But just like with “prototypal inheritance”, “differential inheritance” pretends that your mental model is more important than what is physically happening in the language. It overlooks the fact that object B is not actually differentially constructed, but is instead built with specific characteristics defined, alongside “holes” where nothing is defined. It is in these “holes” (gaps in, or lack of, definition) that delegation can take over and, on the fly, “fill them in” with delegated behavior.

The object is not, by native default, flattened into the single differential object, through copying, that the mental model of “differential inheritance” implies. As such, “differential inheritance” is just not as natural a fit for describing how JavaScript’s [[Prototype]] mechanism actually works.

You can choose to prefer the “differential inheritance” terminology and mental model, as a matter of taste, but there’s no denying the fact that it only fits the mental acrobatics in your mind, not the physical behavior in the engine.

“Constructors”

Let’s go back to some earlier code:

  1. function Foo() {
  2. // ...
  3. }
  4. var a = new Foo();

What exactly leads us to think Foo is a “class”?

For one, we see the use of the new keyword, just like class-oriented languages do when they construct class instances. For another, it appears that we are in fact executing a constructor method of a class, because Foo() is actually a method that gets called, just like how a real class’s constructor gets called when you instantiate that class.

To further the confusion of “constructor” semantics, the arbitrarily labeled Foo.prototype object has another trick up its sleeve. Consider this code:

  1. function Foo() {
  2. // ...
  3. }
  4. Foo.prototype.constructor === Foo; // true
  5. var a = new Foo();
  6. a.constructor === Foo; // true

The Foo.prototype object by default (at declaration time on line 1 of the snippet!) gets a public, non-enumerable (see Chapter 3) property called .constructor, and this property is a reference back to the function (Foo in this case) that the object is associated with. Moreover, we see that object a created by the “constructor” call new Foo() seems to also have a property on it called .constructor which similarly points to “the function which created it”.

Note: This is not actually true. a has no .constructor property on it, and though a.constructor does in fact resolve to the Foo function, “constructor” does not actually mean “was constructed by”, as it appears. We’ll explain this strangeness shortly.

Oh, yeah, also… by convention in the JavaScript world, “class”es are named with a capital letter, so the fact that it’s Foo instead of foo is a strong clue that we intend it to be a “class”. That’s totally obvious to you, right!?

Note: This convention is so strong that many JS linters actually complain if you call new on a method with a lowercase name, or if we don’t call new on a function that happens to start with a capital letter. That sort of boggles the mind that we struggle so much to get (fake) “class-orientation” right in JavaScript that we create linter rules to ensure we use capital letters, even though the capital letter doesn’t mean anything at all to the JS engine.

Constructor Or Call?

In the above snippet, it’s tempting to think that Foo is a “constructor”, because we call it with new and we observe that it “constructs” an object.

In reality, Foo is no more a “constructor” than any other function in your program. Functions themselves are not constructors. However, when you put the new keyword in front of a normal function call, that makes that function call a “constructor call”. In fact, new sort of hijacks any normal function and calls it in a fashion that constructs an object, in addition to whatever else it was going to do.

For example:

  1. function NothingSpecial() {
  2. console.log( "Don't mind me!" );
  3. }
  4. var a = new NothingSpecial();
  5. // "Don't mind me!"
  6. a; // {}

NothingSpecial is just a plain old normal function, but when called with new, it constructs an object, almost as a side-effect, which we happen to assign to a. The call was a constructor call, but NothingSpecial is not, in and of itself, a constructor.

In other words, in JavaScript, it’s most appropriate to say that a “constructor” is any function called with the new keyword in front of it.

Functions aren’t constructors, but function calls are “constructor calls” if and only if new is used.

Mechanics

Are those the only common triggers for ill-fated “class” discussions in JavaScript?

Not quite. JS developers have strived to simulate as much as they can of class-orientation:

  1. function Foo(name) {
  2. this.name = name;
  3. }
  4. Foo.prototype.myName = function() {
  5. return this.name;
  6. };
  7. var a = new Foo( "a" );
  8. var b = new Foo( "b" );
  9. a.myName(); // "a"
  10. b.myName(); // "b"

This snippet shows two additional “class-orientation” tricks in play:

  1. this.name = name: adds the .name property onto each object (a and b, respectively; see Chapter 2 about this binding), similar to how class instances encapsulate data values.

  2. Foo.prototype.myName = ...: perhaps the more interesting technique, this adds a property (function) to the Foo.prototype object. Now, a.myName() works, but perhaps surprisingly. How?

In the above snippet, it’s strongly tempting to think that when a and b are created, the properties/functions on the Foo.prototype object are copied over to each of a and b objects. However, that’s not what happens.

At the beginning of this chapter, we explained the [[Prototype]] link, and how it provides the fall-back look-up steps if a property reference isn’t found directly on an object, as part of the default [[Get]] algorithm.

So, by virtue of how they are created, a and b each end up with an internal [[Prototype]] linkage to Foo.prototype. When myName is not found on a or b, respectively, it’s instead found (through delegation, see Chapter 6) on Foo.prototype.

“Constructor” Redux

Recall the discussion from earlier about the .constructor property, and how it seems like a.constructor === Foo being true means that a has an actual .constructor property on it, pointing at Foo? Not correct.

This is just unfortunate confusion. In actuality, the .constructor reference is also delegated up to Foo.prototype, which happens to, by default, have a .constructor that points at Foo.

It seems awfully convenient that an object a “constructed by” Foo would have access to a .constructor property that points to Foo. But that’s nothing more than a false sense of security. It’s a happy accident, almost tangentially, that a.constructor happens to point at Foo via this default [[Prototype]] delegation. There are actually several ways that the ill-fated assumption of .constructor meaning “was constructed by” can come back to bite you.

For one, the .constructor property on Foo.prototype is only there by default on the object created when Foo the function is declared. If you create a new object, and replace a function’s default .prototype object reference, the new object will not by default magically get a .constructor on it.

Consider:

  1. function Foo() { /* .. */ }
  2. Foo.prototype = { /* .. */ }; // create a new prototype object
  3. var a1 = new Foo();
  4. a1.constructor === Foo; // false!
  5. a1.constructor === Object; // true!

Object(..) didn’t “construct” a1 did it? It sure seems like Foo() “constructed” it. Many developers think of Foo() as doing the construction, but where everything falls apart is when you think “constructor” means “was constructed by”, because by that reasoning, a1.constructor should be Foo, but it isn’t!

What’s happening? a1 has no .constructor property, so it delegates up the [[Prototype]] chain to Foo.prototype. But that object doesn’t have a .constructor either (like the default Foo.prototype object would have had!), so it keeps delegating, this time up to Object.prototype, the top of the delegation chain. That object indeed has a .constructor on it, which points to the built-in Object(..) function.

Misconception, busted.

Of course, you can add .constructor back to the Foo.prototype object, but this takes manual work, especially if you want to match native behavior and have it be non-enumerable (see Chapter 3).

For example:

  1. function Foo() { /* .. */ }
  2. Foo.prototype = { /* .. */ }; // create a new prototype object
  3. // Need to properly "fix" the missing `.constructor`
  4. // property on the new object serving as `Foo.prototype`.
  5. // See Chapter 3 for `defineProperty(..)`.
  6. Object.defineProperty( Foo.prototype, "constructor" , {
  7. enumerable: false,
  8. writable: true,
  9. configurable: true,
  10. value: Foo // point `.constructor` at `Foo`
  11. } );

That’s a lot of manual work to fix .constructor. Moreover, all we’re really doing is perpetuating the misconception that “constructor” means “was constructed by”. That’s an expensive illusion.

The fact is, .constructor on an object arbitrarily points, by default, at a function who, reciprocally, has a reference back to the object — a reference which it calls .prototype. The words “constructor” and “prototype” only have a loose default meaning that might or might not hold true later. The best thing to do is remind yourself, “constructor does not mean constructed by”.

.constructor is not a magic immutable property. It is non-enumerable (see snippet above), but its value is writable (can be changed), and moreover, you can add or overwrite (intentionally or accidentally) a property of the name constructor on any object in any [[Prototype]] chain, with any value you see fit.

By virtue of how the [[Get]] algorithm traverses the [[Prototype]] chain, a .constructor property reference found anywhere may resolve quite differently than you’d expect.

See how arbitrary its meaning actually is?

The result? Some arbitrary object-property reference like a1.constructor cannot actually be trusted to be the assumed default function reference. Moreover, as we’ll see shortly, just by simple omission, a1.constructor can even end up pointing somewhere quite surprising and insensible.

.constructor is extremely unreliable, and an unsafe reference to rely upon in your code. Generally, such references should be avoided where possible.