Classes

From nearly the beginning of JavaScript, syntax and development patterns have all strived (read: struggled) to put on a facade of supporting class-oriented development. With things like new and instanceof and a .constructor property, who couldn’t help but be teased that JS had classes hidden somewhere inside its prototype system?

Of course, JS “classes” aren’t nearly the same as classical classes. The differences are well documented, so I won’t belabor that point any further here.

Note: To learn more about the patterns used in JS to fake “classes,” and an alternative view of prototypes called “delegation,” see the second half of the this & Object Prototypes title of this series.

class

Although JS’s prototype mechanism doesn’t work like traditional classes, that doesn’t stop the strong tide of demand on the language to extend the syntactic sugar so that expressing “classes” looks more like real classes. Enter the ES6 class keyword and its associated mechanism.

This feature is the result of a highly contentious and drawn-out debate, and represents a smaller subset compromise from several strongly opposed views on how to approach JS classes. Most developers who want full classes in JS will find parts of the new syntax quite inviting, but will find important bits still missing. Don’t worry, though. TC39 is already working on additional features to augment classes in the post-ES6 timeframe.

At the heart of the new ES6 class mechanism is the class keyword, which identifies a block where the contents define the members of a function’s prototype. Consider:

  1. class Foo {
  2. constructor(a,b) {
  3. this.x = a;
  4. this.y = b;
  5. }
  6. gimmeXY() {
  7. return this.x * this.y;
  8. }
  9. }

Some things to note:

  • class Foo implies creating a (special) function of the name Foo, much like you did pre-ES6.
  • constructor(..) identifies the signature of that Foo(..) function, as well as its body contents.
  • Class methods use the same “concise method” syntax available to object literals, as discussed in Chapter 2. This also includes the concise generator form as discussed earlier in this chapter, as well as the ES5 getter/setter syntax. However, class methods are non-enumerable whereas object methods are by default enumerable.
  • Unlike object literals, there are no commas separating members in a class body! In fact, they’re not even allowed.

The class syntax definition in the previous snippet can be roughly thought of as this pre-ES6 equivalent, which probably will look fairly familiar to those who’ve done prototype-style coding before:

  1. function Foo(a,b) {
  2. this.x = a;
  3. this.y = b;
  4. }
  5. Foo.prototype.gimmeXY = function() {
  6. return this.x * this.y;
  7. }

In either the pre-ES6 form or the new ES6 class form, this “class” can now be instantiated and used just as you’d expect:

  1. var f = new Foo( 5, 15 );
  2. f.x; // 5
  3. f.y; // 15
  4. f.gimmeXY(); // 75

Caution! Though class Foo seems much like function Foo(), there are important differences:

  • A Foo(..) call of class Foo must be made with new, as the pre-ES6 option of Foo.call( obj ) will not work.
  • While function Foo is “hoisted” (see the Scope & Closures title of this series), class Foo is not; the extends .. clause specifies an expression that cannot be “hoisted.” So, you must declare a class before you can instantiate it.
  • class Foo in the top global scope creates a lexical Foo identifier in that scope, but unlike function Foo does not create a global object property of that name.

The established instanceof operator still works with ES6 classes, because class just creates a constructor function of the same name. However, ES6 introduces a way to customize how instanceof works, using Symbol.hasInstance (see “Well-Known Symbols” in Chapter 7).

Another way of thinking about class, which I find more convenient, is as a macro that is used to automatically populate a prototype object. Optionally, it also wires up the [[Prototype]] relationship if using extends (see the next section).

An ES6 class isn’t really an entity itself, but a meta concept that wraps around other concrete entities, such as functions and properties, and ties them together.

Tip: In addition to the declaration form, a class can also be an expression, as in: var x = class Y { .. }. This is primarily useful for passing a class definition (technically, the constructor itself) as a function argument or assigning it to an object property.

extends and super

ES6 classes also have syntactic sugar for establishing the [[Prototype]] delegation link between two function prototypes — commonly mislabeled “inheritance” or confusingly labeled “prototype inheritance” — using the class-oriented familiar terminology extends:

  1. class Bar extends Foo {
  2. constructor(a,b,c) {
  3. super( a, b );
  4. this.z = c;
  5. }
  6. gimmeXYZ() {
  7. return super.gimmeXY() * this.z;
  8. }
  9. }
  10. var b = new Bar( 5, 15, 25 );
  11. b.x; // 5
  12. b.y; // 15
  13. b.z; // 25
  14. b.gimmeXYZ(); // 1875

A significant new addition is super, which is actually something not directly possible pre-ES6 (without some unfortunate hack trade-offs). In the constructor, super automatically refers to the “parent constructor,” which in the previous example is Foo(..). In a method, it refers to the “parent object,” such that you can then make a property/method access off it, such as super.gimmeXY().

Bar extends Foo of course means to link the [[Prototype]] of Bar.prototype to Foo.prototype. So, super in a method like gimmeXYZ() specifically means Foo.prototype, whereas super means Foo when used in the Bar constructor.

Note: super is not limited to class declarations. It also works in object literals, in much the same way we’re discussing here. See “Object super“ in Chapter 2 for more information.

There Be super Dragons

It is not insignificant to note that super behaves differently depending on where it appears. In fairness, most of the time, that won’t be a problem. But surprises await if you deviate from a narrow norm.

There may be cases where in the constructor you would want to reference the Foo.prototype, such as to directly access one of its properties/methods. However, super in the constructor cannot be used in that way; super.prototype will not work. super(..) means roughly to call new Foo(..), but isn’t actually a usable reference to Foo itself.

Symmetrically, you may want to reference the Foo(..) function from inside a non-constructor method. super.constructor will point at Foo(..) the function, but beware that this function can only be invoked with new. new super.constructor(..) would be valid, but it wouldn’t be terribly useful in most cases, because you can’t make that call use or reference the current this object context, which is likely what you’d want.

Also, super looks like it might be driven by a function’s context just like this — that is, that they’d both be dynamically bound. However, super is not dynamic like this is. When a constructor or method makes a super reference inside it at declaration time (in the class body), that super is statically bound to that specific class hierarchy, and cannot be overridden (at least in ES6).

What does that mean? It means that if you’re in the habit of taking a method from one “class” and “borrowing” it for another class by overriding its this, say with call(..) or apply(..), that may very well create surprises if the method you’re borrowing has a super in it. Consider this class hierarchy:

  1. class ParentA {
  2. constructor() { this.id = "a"; }
  3. foo() { console.log( "ParentA:", this.id ); }
  4. }
  5. class ParentB {
  6. constructor() { this.id = "b"; }
  7. foo() { console.log( "ParentB:", this.id ); }
  8. }
  9. class ChildA extends ParentA {
  10. foo() {
  11. super.foo();
  12. console.log( "ChildA:", this.id );
  13. }
  14. }
  15. class ChildB extends ParentB {
  16. foo() {
  17. super.foo();
  18. console.log( "ChildB:", this.id );
  19. }
  20. }
  21. var a = new ChildA();
  22. a.foo(); // ParentA: a
  23. // ChildA: a
  24. var b = new ChildB(); // ParentB: b
  25. b.foo(); // ChildB: b

All seems fairly natural and expected in this previous snippet. However, if you try to borrow b.foo() and use it in the context of a — by virtue of dynamic this binding, such borrowing is quite common and used in many different ways, including mixins most notably — you may find this result an ugly surprise:

  1. // borrow `b.foo()` to use in `a` context
  2. b.foo.call( a ); // ParentB: a
  3. // ChildB: a

As you can see, the this.id reference was dynamically rebound so that : a is reported in both cases instead of : b. But b.foo()‘s super.foo() reference wasn’t dynamically rebound, so it still reported ParentB instead of the expected ParentA.

Because b.foo() references super, it is statically bound to the ChildB/ParentB hierarchy and cannot be used against the ChildA/ParentA hierarchy. There is no ES6 solution to this limitation.

super seems to work intuitively if you have a static class hierarchy with no cross-pollination. But in all fairness, one of the main benefits of doing this-aware coding is exactly that sort of flexibility. Simply, class + super requires you to avoid such techniques.

The choice boils down to narrowing your object design to these static hierarchies — class, extends, and super will be quite nice — or dropping all attempts to “fake” classes and instead embrace dynamic and flexible, classless objects and [[Prototype]] delegation (see the this & Object Prototypes title of this series).

Subclass Constructor

Constructors are not required for classes or subclasses; a default constructor is substituted in both cases if omitted. However, the default substituted constructor is different for a direct class versus an extended class.

Specifically, the default subclass constructor automatically calls the parent constructor, and passes along any arguments. In other words, you could think of the default subclass constructor sort of like this:

  1. constructor(...args) {
  2. super(...args);
  3. }

This is an important detail to note. Not all class languages have the subclass constructor automatically call the parent constructor. C++ does, but Java does not. But more importantly, in pre-ES6 classes, such automatic “parent constructor” calling does not happen. Be careful when converting to ES6 class if you’ve been relying on such calls not happening.

Another perhaps surprising deviation/limitation of ES6 subclass constructors: in a constructor of a subclass, you cannot access this until super(..) has been called. The reason is nuanced and complicated, but it boils down to the fact that the parent constructor is actually the one creating/initializing your instance’s this. Pre-ES6, it works oppositely; the this object is created by the “subclass constructor,” and then you call a “parent constructor” with the context of the “subclass” this.

Let’s illustrate. This works pre-ES6:

  1. function Foo() {
  2. this.a = 1;
  3. }
  4. function Bar() {
  5. this.b = 2;
  6. Foo.call( this );
  7. }
  8. // `Bar` "extends" `Foo`
  9. Bar.prototype = Object.create( Foo.prototype );

But this ES6 equivalent is not allowed:

  1. class Foo {
  2. constructor() { this.a = 1; }
  3. }
  4. class Bar extends Foo {
  5. constructor() {
  6. this.b = 2; // not allowed before `super()`
  7. super(); // to fix swap these two statements
  8. }
  9. }

In this case, the fix is simple. Just swap the two statements in the subclass Bar constructor. However, if you’ve been relying pre-ES6 on being able to skip calling the “parent constructor,” beware because that won’t be allowed anymore.

extending Natives

One of the most heralded benefits to the new class and extend design is the ability to (finally!) subclass the built-in natives, like Array. Consider:

  1. class MyCoolArray extends Array {
  2. first() { return this[0]; }
  3. last() { return this[this.length - 1]; }
  4. }
  5. var a = new MyCoolArray( 1, 2, 3 );
  6. a.length; // 3
  7. a; // [1,2,3]
  8. a.first(); // 1
  9. a.last(); // 3

Prior to ES6, a fake “subclass” of Array using manual object creation and linking to Array.prototype only partially worked. It missed out on the special behaviors of a real array, such as the automatically updating length property. ES6 subclasses should fully work with “inherited” and augmented behaviors as expected!

Another common pre-ES6 “subclass” limitation is with the Error object, in creating custom error “subclasses.” When genuine Error objects are created, they automatically capture special stack information, including the line number and file where the error is created. Pre-ES6 custom error “subclasses” have no such special behavior, which severely limits their usefulness.

ES6 to the rescue:

  1. class Oops extends Error {
  2. constructor(reason) {
  3. super(reason);
  4. this.oops = reason;
  5. }
  6. }
  7. // later:
  8. var ouch = new Oops( "I messed up!" );
  9. throw ouch;

The ouch custom error object in this previous snippet will behave like any other genuine error object, including capturing stack. That’s a big improvement!

new.target

ES6 introduces a new concept called a meta property (see Chapter 7), in the form of new.target.

If that looks strange, it is; pairing a keyword with a . and a property name is definitely an out-of-the-ordinary pattern for JS.

new.target is a new “magical” value available in all functions, though in normal functions it will always be undefined. In any constructor, new.target always points at the constructor that new actually directly invoked, even if the constructor is in a parent class and was delegated to by a super(..) call from a child constructor. Consider:

  1. class Foo {
  2. constructor() {
  3. console.log( "Foo: ", new.target.name );
  4. }
  5. }
  6. class Bar extends Foo {
  7. constructor() {
  8. super();
  9. console.log( "Bar: ", new.target.name );
  10. }
  11. baz() {
  12. console.log( "baz: ", new.target );
  13. }
  14. }
  15. var a = new Foo();
  16. // Foo: Foo
  17. var b = new Bar();
  18. // Foo: Bar <-- respects the `new` call-site
  19. // Bar: Bar
  20. b.baz();
  21. // baz: undefined

The new.target meta property doesn’t have much purpose in class constructors, except accessing a static property/method (see the next section).

If new.target is undefined, you know the function was not called with new. You can then force a new invocation if that’s necessary.

static

When a subclass Bar extends a parent class Foo, we already observed that Bar.prototype is [[Prototype]]-linked to Foo.prototype. But additionally, Bar() is [[Prototype]]-linked to Foo(). That part may not have such an obvious reasoning.

However, it’s quite useful in the case where you declare static methods (not just properties) for a class, as these are added directly to that class’s function object, not to the function object’s prototype object. Consider:

  1. class Foo {
  2. static cool() { console.log( "cool" ); }
  3. wow() { console.log( "wow" ); }
  4. }
  5. class Bar extends Foo {
  6. static awesome() {
  7. super.cool();
  8. console.log( "awesome" );
  9. }
  10. neat() {
  11. super.wow();
  12. console.log( "neat" );
  13. }
  14. }
  15. Foo.cool(); // "cool"
  16. Bar.cool(); // "cool"
  17. Bar.awesome(); // "cool"
  18. // "awesome"
  19. var b = new Bar();
  20. b.neat(); // "wow"
  21. // "neat"
  22. b.awesome; // undefined
  23. b.cool; // undefined

Be careful not to get confused that static members are on the class’s prototype chain. They’re actually on the dual/parallel chain between the function constructors.

Symbol.species Constructor Getter

One place where static can be useful is in setting the Symbol.species getter (known internally in the specification as @@species) for a derived (child) class. This capability allows a child class to signal to a parent class what constructor should be used — when not intending the child class’s constructor itself — if any parent class method needs to vend a new instance.

For example, many methods on Array create and return a new Array instance. If you define a derived class from Array, but you want those methods to continue to vend actual Array instances instead of from your derived class, this works:

  1. class MyCoolArray extends Array {
  2. // force `species` to be parent constructor
  3. static get [Symbol.species]() { return Array; }
  4. }
  5. var a = new MyCoolArray( 1, 2, 3 ),
  6. b = a.map( function(v){ return v * 2; } );
  7. b instanceof MyCoolArray; // false
  8. b instanceof Array; // true

To illustrate how a parent class method can use a child’s species declaration somewhat like Array#map(..) is doing, consider:

  1. class Foo {
  2. // defer `species` to derived constructor
  3. static get [Symbol.species]() { return this; }
  4. spawn() {
  5. return new this.constructor[Symbol.species]();
  6. }
  7. }
  8. class Bar extends Foo {
  9. // force `species` to be parent constructor
  10. static get [Symbol.species]() { return Foo; }
  11. }
  12. var a = new Foo();
  13. var b = a.spawn();
  14. b instanceof Foo; // true
  15. var x = new Bar();
  16. var y = x.spawn();
  17. y instanceof Bar; // false
  18. y instanceof Foo; // true

The parent class Symbol.species does return this to defer to any derived class, as you’d normally expect. Bar then overrides to manually declare Foo to be used for such instance creation. Of course, a derived class can still vend instances of itself using new this.constructor(..).