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:
let PersonClass = class {
// equivalent of the PersonType constructor
constructor(name) {
this.name = name;
}
// equivalent of PersonType.prototype.sayName
sayName() {
console.log(this.name);
}
};
let person = new PersonClass("Nicholas");
person.sayName(); // outputs "Nicholas"
console.log(person instanceof PersonClass); // true
console.log(person instanceof Object); // true
console.log(typeof PersonClass); // "function"
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:
let PersonClass = class PersonClass2 {
// equivalent of the PersonType constructor
constructor(name) {
this.name = name;
}
// equivalent of PersonType.prototype.sayName
sayName() {
console.log(this.name);
}
};
console.log(typeof PersonClass); // "function"
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:
// direct equivalent of PersonClass named class expression
let PersonClass = (function() {
"use strict";
const PersonClass2 = function(name) {
// make sure the function was called with new
if (typeof new.target === "undefined") {
throw new Error("Constructor must be called with new.");
}
this.name = name;
}
Object.defineProperty(PersonClass2.prototype, "sayName", {
value: function() {
// make sure the method wasn't called with new
if (typeof new.target !== "undefined") {
throw new Error("Method cannot be called with new.");
}
console.log(this.name);
},
enumerable: false,
writable: true,
configurable: true
});
return PersonClass2;
}());
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.