Fred Brack   Fred Brack  
Raleigh, NC
Picture of the Cape Hatteras lighthouse

JavaScript Classes
by Fred Brack
Updated

JavaScript LogoThis is not a full tutorial but rather an introduction and reference to the basic features of JavaScript Classes.  For more detail, JavaScript.info is a great teaching aid or reference.  And I do love W3Schools.  I created this series of JavaScript web pages (see the index at the bottom) to help me document my own understanding of the language as I progressed from novice to intermediate user ... and because I like to document stuff for others!  I will come back to it often as a reference document.  I hope you find it useful.

Introduction to Classes

JavaScript Classes effectively implement a basic concept of Object-Oriented Programming.  According to Wikipedia:

Languages that support object-oriented programming (OOP) typically use inheritance for code reuse and extensibility in the form of either classes or prototypes. Those that use classes support two main concepts:

JavaScript implements all of this:  classes, methods, and objects.

A Class is a generic description of an object, and its implementation in JavaScript defines the basics (properties and methods) and lets you instantiate new instances of an object with unique data multiple times.  It does this using the JavaScript concept of a prototype (a generic description of an object) and something called the constructor method.  We are not going to discuss prototypes here, though, so we can refer you elsewhere to learn about prototypes and inheritance.  We are going to discuss the "modern" shortcut method, class, whose underpinnings rely upon prototype concepts.

Defining a Class

Defining a class requires the use of the constructor method within the class construct.  Once you have defined a class, you instantiate a new instance of the class by using the "new" function; and it turns out that "new" knows to call the constructor method automatically!  A bit confusing, but read on.

Let's get started by defining a class for car models; but first, some important guidelines!

  1. By convention, class names have an initial cap, so our class will be named Car.
  2. Next, we have to use the constructor method to "construct" our class object definitions.  As a function, it builds the property:value relationships based on passed parameters.  By referencing this (and "this" is explained here), it refers to the current instantiation of any resulting object.
  3. And finally, we have to supply any methods required by our objects.
  4. But please observe these caveats:

We'll start out simple, defining a Car class with only model and year and adding only one method to display them.

class Car {
  constructor (modelx, yearx) { 
    this.model = modelx;
    this.year = yearx;
  }
  sayAll() { console.log(this.year+" "+this.model) }
}

let myCar = new Car("Ford",2020); // here we instantiate our class
myCar.sayAll(); // = 2020 Ford

Although we aren't going to cover these details, we should point out that Car is effectively the constructor method of Car.prototype; that is, "Car === Car.prototype.constructor", and the method(s) is/are in the Car.prototype (e.g., Car.prototype.sayAll).

Declaring Property Names

It is possible that you would like to create some default values (property:value combinations) in your class definition.  You do this by declaring variable names prior to calling the constructor, which guarantees any instance will have these property names even if not initialized by the constructor.  If you just declare a variable (property name) by itself, you are simply stating that this is a valid property that the user can set and access later (considered good practice for clarity); but you can also initialize a value.  Example:

class Car {
  model; // merely declaring
  year = "????"; // declaring and initializing a default value
  constructor (modelx, yearx) { 
    this.model = modelx;
    if (typeof(yearx)=="number") {this.year = yearx;} // may or may not set here
  }
  sayAll() { console.log(this.year+" "+this.model) }
}
let myCar = new Car("Ford"); // instantiate our class w/o a year
myCar.sayAll(); // = ???? Ford

Other Types of Fields

CAUTION:  There is very little documentation on static fields, so this section should be viewed as FYI rather than recommended for practical use.  They may not work in all environments.

You can also create static fields in your class definition by using the static keyword.  You might create a static field if the value is fixed for all instances, but you don't want it to actually be created for all instances.  For this reason, to access the static field you must precede its name with the class name, not the name of the instance.  Redoing our first example:

class Car {
  static type = "Domestic";
  constructor (model, year) { 
    this.model = model;
    this.year = year;
  }
  sayAll() { console.log(Car.type+" "+this.year+" "+this.model) }
}
let myCar = new Car("Ford",2020); 
myCar.sayAll(); // = Domestic 2020 Ford
console.log("Car type is "+Car.type); // = Car type is Domestic

Note:  Some people recommend that static variables be written in ALL CAPS.  Thus in our example, the variable name would be TYPE.

[CAUTION:  Not recommended!]  There is another convention you should be aware of for future reference, and that is private fields, part of the encapsulation concept.  If you want to use a variable inside your class definition but not have it available externally, you prefix any such variable name with a hash tag like this:  #workValue.  This technique is currently (2020) experimental and may not work with all browsers.

[Again, not recommended yet.]  Combining the two previous concepts, you can make a private static field by preceding its name with a hash tag.  Why would you do this?  First you should understand that the term static means the field stays with the class definition, not the instance.  It does not mean the field value is necessarily static!  So you can change the value of a static field at the class level, but not at the instance level.  Thus if you wanted to count and perhaps limit the number of instantiations of a class, you could define two internal variables, say #instances and #maxInstances, incrementing #instances in the constructor and comparing it there with #maxInstances to possibly kick out an error message.  You can also make a private class method (only used internally by the constructor) by preceding the function name with a hash tag.  (Credit for this example and other information in this section goes to Dmitri Pavlutin.)

Extending a Class

Once we have defined a class (as we did above), we can optionally extend it to create a second class.  The second class would essentially inherit all the characteristics of the first class, then add properties and/or methods of its own.

Let's look at extending the Car class to become a ForeignCar class.  To do this we extend Car by using the extends keyword in the new class definition of ForeignCar to indicate we want to extend it using the class name specified after 'extends' as a base; then we use the super method (as in superior, I guess) to call back to the class to be extended to handle what it can; and we follow that with any new properties or methods for our newly created class.  Example:

class ForeignCar extends Car {
  constructor(modelx, yearx, countryx) {
    super(modelx, yearx); // use Car to handle what it knows about
    this.country = countryx; // then ADD a property (country) here
  }
}

Function Borrowing

Function borrowing is the practice of "borrowing" a function definition in one object for use in another object.  You "borrow" instead of inherit because you only want that one property (method) from the other object, not all the properties.  You also "borrow" instead of copy so you only have to maintain one copy of the function definition.  You accomplish this objective by suffixing a call to the original function (method) with one of three borrowing methods:  .call, .apply, or .bind.  In the following example taken from medium.com, a "dog" class was defined which included a function called tellUsAboutYourself which summarized the dog's characteristics (properties like "breed") in a sentence.  Then a "cat" class was defined using similar cat characteristic properties.  A dog class was instantiated as "fido" and a cat as "sparkles".  Rather than recode or attempt to inherit the tellUsAboutYourself function, it gets "borrowed" like this:

fido.tellUsAboutYourSelf.call(sparkles)
------------------------ --------------
the dog class function...called on behalf of the cat

.apply works similarly to .call but has the subtle difference that its operands can be an array, if you need to pass multiple operands.  Use .bind to create a shortcut for using another object's function against whatever you want.

const describeSparkles = fido.tellUsAboutYourSelf.bind(sparkles)
describeSparkles() // yields the same result as the previous example

Object Accessors (Get and Set)

Object Accessors are called getters and setters (implemented as get and set within the class definition, after the constructor).  The get syntax binds an object property to a function that will be called when that property is looked up, while the set syntax does something similar when an attempt is made to change or establish an object property.  Here's an implementation:

class Car {
  constructor (modelx, yearx) { 
    this.model = modelx;
    this.year = yearx;
  }
  get model() { return this.modelx; }
  set model(value) { this.modelx = value }
  sayAll() { console.log(this.year+" "+this.model) }
}
let myCar = new Car("Ford",2020); // instantiate our class
console.log("My car model is a "+myCar.model); // = ... Ford; invokes the getter
myCar.model = "Chevrolet"; // invokes the setter
console.log("My car model is a "+myCar.model); // ... Chevrolet

What's the value?  It may be easier to picture placing error detection (such as a missing value like year) in one of these functions, rather than building it into the constructor.

Note that we have made the get and set names the same as the property names in our example, because this seems to make sense -- you are getting or setting that property.  However, you do not have to do this.  You may give get and/or set a different name and treat it as if it were a property name (not a method).  For example, if we completed the example above with a getter and setter for year and added a getter for all, we could replicate the effect of the sayAll method:

 get year() { return this.yearx }
set year(value) { this.yearx = value }
get all() { return this.yearx+" "+this.modelx; }
// then execute the following
console.log(myCar.all); // = 2020 Chevrolet

It's a subtle point, but invoking the setter all (via myCar.all) does not require empty parens for an operand, as the sayAll function does (myCar.sayAll()).

# # # # #

Fred