Class Expressions

Classes and functions are similar in that they have two forms: declarations and expressions. Function and class declarations begin with an appropriate keyword (function or class, respectively) followed by an identifier. Functions have an expression form that doesn’t require an identifier after function, and similarly, classes have an expression form that doesn’t require an identifier after class. These class expressions are designed to be used in variable declarations or passed into functions as arguments.

A Basic Class Expression

Here’s the class expression equivalent of the previous PersonClass examples, followed by some code that uses it:

  1. let PersonClass = class {
  2. // equivalent of the PersonType constructor
  3. constructor(name) {
  4. this.name = name;
  5. }
  6. // equivalent of PersonType.prototype.sayName
  7. sayName() {
  8. console.log(this.name);
  9. }
  10. };
  11. let person = new PersonClass("Nicholas");
  12. person.sayName(); // outputs "Nicholas"
  13. console.log(person instanceof PersonClass); // true
  14. console.log(person instanceof Object); // true
  15. console.log(typeof PersonClass); // "function"
  16. console.log(typeof PersonClass.prototype.sayName); // "function"

As this example demonstrates, class expressions do not require identifiers after class. Aside from the syntax, class expressions are functionally equivalent to class declarations.

Whether you use class declarations or class expressions is mostly a matter of style. Unlike function declarations and function expressions, both class declarations and class expressions are not hoisted, and so the choice has little bearing on the runtime behavior of the code.

Named Class Expressions

The previous section used an anonymous class expression in the example, but just like function expressions, you can also name class expressions. To do so, include an identifier after the class keyword like this:

  1. let PersonClass = class PersonClass2 {
  2. // equivalent of the PersonType constructor
  3. constructor(name) {
  4. this.name = name;
  5. }
  6. // equivalent of PersonType.prototype.sayName
  7. sayName() {
  8. console.log(this.name);
  9. }
  10. };
  11. console.log(typeof PersonClass); // "function"
  12. console.log(typeof PersonClass2); // "undefined"

In this example, the class expression is named PersonClass2. The PersonClass2 identifier exists only within the class definition so that it can be used inside the class methods (such as the sayName() method in this example). Outside the class, typeof PersonClass2 is "undefined" because no PersonClass2 binding exists there. To understand why this is, look at an equivalent declaration that doesn’t use classes:

  1. // direct equivalent of PersonClass named class expression
  2. let PersonClass = (function() {
  3. "use strict";
  4. const PersonClass2 = function(name) {
  5. // make sure the function was called with new
  6. if (typeof new.target === "undefined") {
  7. throw new Error("Constructor must be called with new.");
  8. }
  9. this.name = name;
  10. }
  11. Object.defineProperty(PersonClass2.prototype, "sayName", {
  12. value: function() {
  13. // make sure the method wasn't called with new
  14. if (typeof new.target !== "undefined") {
  15. throw new Error("Method cannot be called with new.");
  16. }
  17. console.log(this.name);
  18. },
  19. enumerable: false,
  20. writable: true,
  21. configurable: true
  22. });
  23. return PersonClass2;
  24. }());

Creating a named class expression slightly changes what’s happening in the JavaScript engine. For class declarations, the outer binding (defined with let) has the same name as the inner binding (defined with const). A named class expression uses its name in the const definition, so PersonClass2 is defined for use only inside the class.

While named class expressions behave differently from named function expressions, there are still a lot of similarities between the two. Both can be used as values, and that opens up a lot of possibilities, which I’ll cover next.