More About Classes

There's more to learn about classes. In this chapter, we'll discuss:

  • Private fields and methods
  • Getters and setters
  • Static fields and methods

Private Fields and Methods

By default, object properties and methods are public, which means they can be accessed at any time by a user of an instance:

let database = {
  getStudentId(name) {
    // This method should probably access a database
    return 4201567;
  }
};

class Student {
  constructor(firstName, lastName, track) {
    this.name = [firstName, lastName];
    this.track = track;
  }

  // This method shouldn't be public!
  revealStudentId() {
    let studentId = database.getStudentId(this.name);
    console.log(studentId);
  }
}

let student = new Student('Kay', 'Oakley', 'JavaScript');
console.log(`${student.name.join(' ')} ${student.track}`);
// Kay Oakley JavaScript

// Reveals sensitive info about student
student.revealStudentId(); // 4201567

student.name = ['Bill', 'Wisner'];
student.track = 'Ruby';
console.log(`${student.name.join(' ')} ${student.track}`);
// Bill Wisner Ruby

While such openness provides considerable flexibility, it can lead to bugs or reveal sensitive information. It may also make it hard to update your classes and types. When other developers use your code and come to rely on the properties defined for your objects, there will be complaints if you try to change the way things are done.

One solution to these problems is to use private fields and methods.

Ordinary object properties are referred to by various terms, such as instance properties, public instance properties, or, confusingly, public class fields. Similarly, ordinary object methods may be called instance methods, public instance methods, or public class methods. These variations in terminology can be very confusing as you browse the various resources available for JavaScript.

Private Fields

Private fields, also known as private instance properties or, confusingly, private class fields, are object properties that are private to an object. No other objects can access them directly. This aids encapsulation and prevents your class's users from misusing your object properties. Private fields help you to change your implementation without breaking other users' code.

Private fields are accessed like instance properties, except the name of the property is prefixed by a # character. In addition, you must declare (and optionally initialize) the field at the class level:

class Foo {
  //highlight
  // Next 2 lines declare #data and #initializedData fields
  #data;                      // uninitialized private field
  #initializedData = 43;      // initialized private field

  constructor(value) {
    this.#data = value;
  }

  show() {
    console.log(this.#data, this.#initializedData);
  }
  //endhighlight
}

//highlight
let foo = new Foo(42);
foo.show();         // 42 43
//endhighlight

Thus, we can rewrite the Student class above to use private fields:

class Student {
  #name;
  #track;

  constructor(firstName, lastName, track) {
    this.#name = [firstName, lastName];
    this.#track = track;
  }
}

let student = new Student('Kay', 'Oakley', 'JavaScript');
//highlight
console.log(`${student.#name.join(' ')} ${student.#track}`);
// SyntaxError: Private field '#name' must be declared in an
// enclosing class
//endhighlight

As you can see, we can't access the private fields outside of the class. If you want to allow your users to access the values in the private fields, you can define methods to retrieve the values of the #name and #track properties:

class Student {
  #name;
  #track;

  constructor(firstName, lastName, track) {
    this.#name = [firstName, lastName];
    this.#track = track;
  }

  //highlight
  name() {
    return this.#name;
  }

  track() {
    return this.#track;
  }
  //endhighlight
}

let student = new Student('Kay', 'Oakley', 'JavaScript');
console.log(`${student.name().join(' ')} ${student.track()}`);
//highlight
// Kay Oakley JavaScript
//endhighlight

You can also define methods to set the values of the private instance properties. However, we won't do that now. We'll talk about that when we talk about setters.

Private instance properties are a relatively recent feature of JavaScript. The feature was finalized in 2022. As of 2024, all major browsers and Node have added them, but older versions may not yet support them.

Private Methods

Private methods, also known as private instance methods or, confusingly, private class methods, are instance methods that are private to an object. These methods can only be used within the class. Otherwise, they look and act like ordinary methods except that their names begin with a # and can't be called from outside the class:

class MyClass {
  myPublic() {
    return this.#myPrivate();
  }

  #myPrivate() {
    return 'This is a private method';
  }
}

const instance = new MyClass();
console.log(instance.myPublic());  // This is a private method
console.log(instance.#myPrivate());
// Error: Private field '#myPrivate' must be declared in an
// enclosing class

Private methods are a great solution when there are certain operations you need inside your objects but don't want to make those operations available to users of your objects:

class Student {
  #name;
  #track;

  constructor(firstName, lastName, track) {
    this.#name = [firstName, lastName];
    this.#track = track;
  }

  #revealStudentId() {
    let studentId = database.getStudentId(this.#name);
    console.log(studentId.idNumber);
  }
}

let student = new Student('Kay', 'Oakley', 'JavaScript');
student.#revealStudentId(); // Raises an error!

Getters and Setters

Private fields aren't always the right choice. Sometimes, you want your object properties to be available to the users of your objects. However, if you allow that, you'll be constrained in your implementation updates in the future. Suppose you decide to change something that affects one of your object properties. In that case, that change will have a ripple effect on the users of your objects who use those properties.

Unrestricted access to your properties also means users can reassign them to anything they want, including values that do not make sense in the application's context. Ideally, you may wish to constrain what values can be assigned to a property.

One solution to these problems is to use getters and setters. Getters and setters are methods that work like ordinary properties; you can access them without parentheses and assign them values. However, they give you a lot more control than ordinary properties would.

Getters

Let's consider our Student class:

class Student {
  constructor(firstName, lastName, track) {
    this.name = [firstName, lastName];
    this.track = track;
  }
}

let student = new Student('Kay', 'Oakley', 'JavaScript');
console.log(`${student.name.join(' ')} ${student.track}`);
// Kay Oakley JavaScript

Since we let users access the properties directly, we've boxed ourselves in. We can no longer change how the various properties are stored. For instance, suppose we decide to store the first and last names separately instead of as an array:

class Student {
  constructor(firstName, lastName, track) {
    //highlight
    this.firstName = firstName;
    this.lastName = lastName;
    //endhighlight
    this.track = track;
  }
}

let student = new Student('Kay', 'Oakley', 'JavaScript');
console.log(`${student.name.join(' ')} ${student.track}`);
// TypeError: Cannot read properties of undefined(reading 'join');

Suddenly, our users will begin experiencing errors or bugs when they try to access the name property as an Array. Changing the way we store our state has changed the class's API.

There are some workarounds we can use:

  • Require users to update their code to use the firstName and lastName properties instead of the name array. This is impolite and may lead users to abandon your code in favor of another, more stable package. Some may decide they won't upgrade, which is bad if you later have to fix a security problem in your code.
  • Create a name() method that returns the first and last names as an Array. Unfortunately, users still have to update their code.
  • Don't change the way your code works. However, there may be good reasons for a change, which may make this impractical.

None of these workarounds are satisfactory. A solution that does work well is to create a getter for the former name property:

class Student {
  constructor(firstName, lastName, track) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.track = track;
  }

  //highlight
  get name() {
    return [this.firstName, this.lastName];
  }
  //endhighlight
}

let student = new Student('Kay', 'Oakley', 'JavaScript');
console.log(`${student.name.join(' ')} ${student.track}`);
// Kay Oakley JavaScript

Bingo! That did the trick.

Getters are a special kind of method you can define in any object. Internally, they look like methods with the keyword get preceding a method definition that uses concise method syntax. However, getters aren't used like methods. Instead, they are used like ordinary instance properties: you reference the getter name to retrieve the corresponding data. You don't have to, nor can you, use the parentheses of a method call.

You can define getters in any object. You don't have to use a class. For instance, in the code below, we've defined a getter to make it easy to get the full name:

let teacher = {
  firstName: 'Alan',
  lastName: 'Stone',

  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  },
};

console.log(teacher.fullName);   // Alan Stone

Consider another use case for getters. Suppose your object has a property whose value is mutable. Suppose you allow direct access to that property. In that case, users can modify the mutable value, which may be undesirable. In that case, you can write a getter that returns a copy of the mutable property's value:

let teacher = {
  firstName: 'Alan',
  lastName: 'Stone',
  _students: ['Pete', 'Brian', 'Andrea', 'Beverly', 'Joel'],

  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  },

  get students() {
    return [...this._students];  // A copy of the _students array
  },
};

console.log(teacher.fullName);   // Alan Stone

let students = teacher.students;
console.log(students);
// [ 'Pete', 'Brian', 'Andrea', 'Beverly', 'Joel' ]

students.pop();
console.log(students);
// [ 'Pete', 'Brian', 'Andrea', 'Beverly' ]

console.log(teacher.students);
// [ 'Pete', 'Brian', 'Andrea', 'Beverly', 'Joel' ]

By returning a copy of the _students array, we can manipulate the return value without impacting the teacher object.

Note that we had to change the name of the students property to _students. If you don't do this, you'll experience a stack overflow due to recursive calls to the students getter. A single leading underscore is a JavaScript convention that marks something as intended for internal use only. So, while users can use either teacher.students or teacher._students to access the _students property, they should only use teacher.students.

You can also create getters that merely return a property's value. For instance, we can add firstName, lastName, and track getters to the Student class. Again, you will have to rename the firstName and lastName properties:

class Student {
  constructor(firstName, lastName, track) {
    //highlight
    this._firstName = firstName;
    this._lastName = lastName;
    this._track = track;
    //endhighlight
  }

  get name() {
    return [this.firstName, this.lastName];
  }

  //highlight
  get firstName() {
    return this._firstName;
  }

  get lastName() {
    return this._lastName;
  }

  get track() {
    return this._track;
  }
//endhighlight
}

let student = new Student('Kay', 'Oakley', 'JavaScript');
console.log(`${student.name.join(' ')} ${student.track}`);
// Kay Oakley JavaScript
console.log(`${student.firstName} ${student.lastName}`);
// Kay Oakley

The practicality of creating such getters is limited. The best use case is preparation for making the actual properties private. We'll see an example of that momentarily.

Note that we don't need the underscores in the name getter. That getter uses the firstName and lastName getters to retrieve the values of the _firstName and _lastName properties.

Instead of the single underscore, you can also use private fields:

class Student {
  //highlight
  #firstName;
  #lastName;
  #track;
  //endhighlight

  constructor(firstName, lastName, track) {
    //highlight
    this.#firstName = firstName;
    this.#lastName = lastName;
    this.#track = track;
    //endhighlight
  }

  get name() {
    return [this.firstName, this.lastName];
  }

  get firstName() {
    //highlight
    return this.#firstName;
    //endhighlight
  }

  get lastName() {
    //highlight
    return this.#lastName;
    //endhighlight
  }

  get track() {
    //highlight
    return this.#track;
    //endhighlight
  }
}

Setters

Let's add some validation for our track property. Its value should be either JavaScript, Python, or Ruby. Anything else should raise an error:

class Student {
  #firstName;
  #lastName;
  #track;

  constructor(firstName, lastName, track) {
    this.#firstName = firstName;
    this.#lastName = lastName;
    //highlight

    switch (track) {
      case 'JavaScript':
      case 'Python':
      case 'Ruby':
        this.#track = track;
        break;
      default:
        throw new Error(`Invalid track: '${track}'`);
    }
    //endhighlight
  }

  get name() {
    return [this.firstName, this.lastName];
  }

  get firstName() {
    return this.#firstName;
  }

  get lastName() {
    return this.#lastName;
  }

  get track() {
    return this.#track;
  }
}

let student2 = new Student('Kay', 'Oakley', 'JavaScript');
console.log(`${student2.name.join(' ')} ${student2.track}`);
// Kay Oakley JavaScript

let student3 = new Student('Bill', 'Wisner', 'Python');
console.log(`${student3.name.join(' ')} ${student3.track}`);
// Bill Wisner Python

let student4 = new Student('Kim', 'Serkes', 'Gnome');
console.log(`${student4.name.join(' ')} ${student4.track}`);
// Invalid track: 'Gnome'

Suppose we want to allow users of the Student class to change the track. We could create a setTrack method that would validate the track name, but class users would have to update their code and call setTrack instead of using a simpler assignment. To address this, we'll create a setter method.

Setters, like getters, are special methods you can define in any object. Internally, a setter looks like a concise syntax method definition preceded by the keyword set. A setter takes a single argument: the value you want to assign to the property. Outside of the method, however, it looks just like an ordinary property: you assign a value to what looks like a property in your object. You don't have to, nor can you, use the parentheses of a method call.

class Student {
  #firstName;
  #lastName;
  #track;

  constructor(firstName, lastName, track) {
    this.#firstName = firstName;
    this.#lastName = lastName;

    switch (track) {
      case 'JavaScript':
      case 'Python':
      case 'Ruby':
        this.#track = track;
        break;
      default:
        throw new Error(`Invalid track: '${track}'`);
    }
  }

  get name() {
    return [this.firstName, this.lastName];
  }

  get firstName() {
    return this.#firstName;
  }

  get lastName() {
    return this.#lastName;
  }

  get track() {
    return this.#track;
  }

  //highlight
  set track(newTrack) {
    switch (newTrack) {
      case 'JavaScript':
      case 'Python':
      case 'Ruby':
        this.#track = newTrack;
        break;
      default:
        throw new Error(`Invalid track: '${newTrack}'`);
    }
  }
  //endhighlight
}

let student2 = new Student('Kay', 'Oakley', 'JavaScript');
console.log(`${student2.name.join(' ')} ${student2.track}`);
// Kay Oakley JavaScript

let student3 = new Student('Bill', 'Wisner', 'Python');
console.log(`${student3.name.join(' ')} ${student3.track}`);
// Bill Wisner Python

//highlight
student3.track = 'Ruby';
console.log(`${student3.name.join(' ')} ${student3.track}`);
// Bill Wisner Ruby

student3.track = 'Baaa!';
console.log(`${student3.name.join(' ')} ${student3.track}`);
// Invalid track: 'Baaa!'
//endhighlight

In this code, the setter begins with the keyword set followed by the property name and a pair of parentheses surrounding a single argument. After that, you have an ordinary function body that sets the value of a property.

Setters, like getters, must be written using syntax that resembles the concise method syntax. You can't use the function keyword when defining a setter.

Like getters, you use setters just like ordinary properties, not methods. In other words, you write student.firstName = something, not student.firstName(something).

We should probably do away with that code duplication in the constructor method and the track setter:

class Student {
  #firstName;
  #lastName;
  #track;

  constructor(firstName, lastName, track) {
    this.#firstName = firstName;
    this.#lastName = lastName;
    //highlight
    this.track = track;  // we're calling the setter here
    //endhighlight
  }

  get name() {
    return [this.firstName, this.lastName];
  }

  get firstName() {
    return this.#firstName;
  }

  get lastName() {
    return this.#lastName;
  }

  get track() {
    return this.#track;
  }

  set track(newTrack) {
    switch (newTrack) {
      case 'JavaScript':
      case 'Python':
      case 'Ruby':
        this.#track = newTrack;
        break;
      default:
        throw new Error(`Invalid track: '${newTrack}'`);
    }
  }
}

let student2 = new Student('Kay', 'Oakley', 'JavaScript');
console.log(`${student2.name.join(' ')} ${student2.track}`);
// Kay Oakley JavaScript

let student3 = new Student('Bill', 'Wisner', 'Python');
console.log(`${student3.name.join(' ')} ${student3.track}`);
// Bill Wisner Python

//highlight
student3.track = 'Ruby';
console.log(`${student3.name.join(' ')} ${student3.track}`);
// Bill Wisner Ruby

student3.track = 'Baaa!';
console.log(`${student3.name.join(' ')} ${student3.track}`);
// Invalid track: 'Baaa!'
//endhighlight

You can define setters in any object. You don't have to be using a class.

let teacher = {
  firstName: 'Alan',
  lastName: 'Stone',

  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  },

  set name(nameArray) {
    this.firstName = nameArray[0]
    this.lastName = nameArray[1]
  },
};

teacher.name = ['Mike', 'Becker']
console.log(teacher.fullName);   // Mike Becker

Static Fields and Methods

In JavaScript, static fields and static methods are properties and methods that belong to the class itself rather than to class instances. They can be accessed directly on the class without needing to instantiate an object of that class.

Static Fields

Static fields, sometimes called static properties or class properties, are properties that are defined on the class itself. They are not tied to an instance of the class. Developers typically use static fields to store constants or utility data shared across all class instances. For example:

class MyClass {
  static myField = 'This is a static field';

  constructor() {
    console.log(MyClass.myField); // Accessible inside the class
                                  // using the class name
  }
}

console.log(MyClass.myField);     // Accessible outside the class
                                  // using the class name

const instance = new MyClass();
console.log(instance.myField);    // undefined: myField is not
                                  // defined on this object.

A common use case for static fields is to keep track of something related to a class, such as an instance count:

class Student {
  static counter = 0;

  constructor(name) {
    this.name = name;
    Student.counter += 1;
  }
}

console.log(Student.counter);           // 0

let ken = new Student('Ken');
console.log(Student.counter);           // 1

let lynn = new Student('Lynn');
console.log(Student.counter);           // 2

Static Methods

Static methods, sometimes called class methods, are defined on a class rather than instance objects. They are not available from the class instances. Static methods typically provide utility functions that perform tasks relevant to the class but do not require access to instance-specific data. For example:

class MyClass {
  static staticMethod() {
    console.log('This is a static method.');
  }

  instanceMethod() {
    console.log('This is an instance method.');
  }
}

// Calling the static method
MyClass.staticMethod();    // This is a static method.

const instance = new MyClass();

instance.instanceMethod(); // This is an instance method.
instance.staticMethod();   // Raises error

A common use case for static methods is to report data relevant to a class:

class Student {
  static counter = 0;

  static showCounter() {
    console.log(`We have created ${Student.counter} students!`);
  }

constructor(name) {
    this.name = name;
    Student.counter += 1;
  }

}

let ken = new Student('Ken');
let lynn = new Student('Lynn');
Student.showCounter();        // We have created 2 students!

Summary

In addition to the usual object properties and object methods, JavaScript classes support several additional flavors of properties and methods. All of the different types of properties and methods are known by several names:

  • Ordinary object properties are known by several synonymous terms: instance properties, public fields, and public class fields. Instance properties belong to a class's instance objects. By default, they are public. Any user of an object can access or update the values of those properties.

  • Likewise, ordinary object methods are variously known as instance methods, public methods, and public class methods. Instance methods belong to instance objects and are, by default, public. Any user of an object can call those methods.

  • Private fields are known by several synonymous terms: private properties, private instance properties, and private class fields. Private fields belong to a class's instance objects but are private: only the methods inside the class can access them. Private fields are named with a leading # and must be declared before they can be used.

  • Likewise, private methods are variously known as private instance methods and private class methods. Private methods belong to a class's instance objects but are private: they can only be called by the instance methods defined by the class. Private methods are named with a leading #.

  • Static fields are variously known as static properties or class properties. They are, by default, public and belong to the class, not instances of the class. You must use the class name or a reference to the class to access static fields; you can't use an instance object.

  • Static methods are sometimes called class methods. They are, by default, public and belong to the class, not instances of the class. You must use the class name or a reference to the class to call the method; you can't use an instance object.

In the next chapter, we'll explore prototypal inheritance and learn how classes relate to this fundamental aspect of JavaScript.

Exercises

  1. Rewrite the following Person class to use private fields for the name and age properties and provide a setter for setting the age property. Ensure that the setter raises a RangeError unless the age is a positive number.

    class Person {
      constructor(name, age) {
        this.name = name;
        this.age = age;
      }
    
      showAge() {
        console.log(this.age);
      }
    }
    
    let person = new Person('John', 30);
    person.showAge(); // 30
    person.age = 31;
    person.showAge(); // 31
    
    try {
      // This line should raise a RangeError,
      // but does not.
      person.age = -5;
      person.showAge(); // -5
    } catch (e) {
      //highlight
      // The following line should run, but won't
      console.log('RangeError: Age must be positive');
      //endhighlight
    }
    

    Solution

    class Person {
      //highlight
      #name;
      #age;
      //endhighlight
    
      constructor(name, age) {
        //highlight
        this.#name = name;
        this.age = age; // Call the setter to validate data
        //endhighlight
      }
    
      //highlight
      set age(age) {
        if (typeof(age) === 'number' && age > 0) {
          this.#age = age;
        } else {
          throw new RangeError('Age must be positive');
        }
      }
      //endhighlight
    
      showAge() {
        //highlight
        console.log(this.#age);
        //endhighlight
      }
    }
    
    let person = new Person('John', 30);
    person.showAge(); // 30
    person.age = 31;
    person.showAge(); // 31
    
    try {
      person.age = -5;
      //highlight
      // The following line will not run
      person.showAge();
      //endhighlight
    } catch (e) {
      //highlight
      // The following line will run
      console.log('RangeError: Age must be positive');
      //endhighlight
    }
    
  2. Create a Book class with private fields title, author, and year. Provide getters for each field and a setter for the year field that raises a RangeError if year is before 1900.

    class Book {
      // your code here
    }
    
    let book = new Book('The Great Gatsby', 'F. Scott Fitzgerald', 1925);
    console.log(book.title);  // The Great Gatsby
    console.log(book.author); // F. Scott Fitzgerald
    console.log(book.year);   // 1925
    
    book.year = 1932;         // Changing year
    console.log(book.year);   // 1932
    
    try {
      book.year = 1825;
    } catch (e) {
      console.log(e);   // RangeError: Invalid year
    }
    
    try {
      let book2 = new Book('A Tale of Two Cities', 'Charles Dickents', 1859);
    } catch (e) {
      console.log(e);   // RangeError: Invalid year
    }
    

    Solution

    class Book {
      //highlight
      #title;
      #author;
      #year;
    
      constructor(title, author, year) {
        this.#title = title;
        this.#author = author;
        this.year = year;
      }
    
      get title() {
        return this.#title;
      }
    
      get author() {
        return this.#author;
      }
    
      get year() {
        return this.#year;
      }
    
      set year(newYear) {
        if (newYear < 1900) {
          throw new RangeError('Invalid year');
        } else {
          this.#year = newYear;
        }
      }
      //endhighlight
    }
    
    // test code omitted for brevity
    
  3. Create a BankAccount class with a private field balance. Add a private method, #checkBalance, that logs the current balance. Provide a public method, deposit, to add money to the account and withdraw to take money out. Raise a RangeError if there are insufficient funds for the withdrawal.

    class BankAccount {
      // your code here
    }
    
    let account = new BankAccount();
    account.deposit(100);
    account.withdraw(50);
    account.withdraw(100); // RangeError: Insufficient funds
    

    Solution

    class BankAccount {
      //highlight
      #balance = 0;
    
      #checkBalance() {
        console.log(`Current balance: $${this.#balance}`);
      }
    
      deposit(amount) {
        this.#balance += amount;
        this.#checkBalance();
      }
    
      withdraw(amount) {
        if (amount > this.#balance) {
          throw new RangeError('Insufficient funds');
        } else {
          this.#balance -= amount;
          this.#checkBalance();
        }
      }
      //endhighlight
    }
    
    let account = new BankAccount();
    account.deposit(100);  // Current balance: $100
    account.withdraw(50);  // Current balance: $50
    account.withdraw(100); // Insufficient funds
    
  4. Create a Rectangle class with private fields width and height. Provide getters and setters for both fields. The setters should raise a RangeError if the width or height is not a positive number. Add a getter for area to compute the area of the rectangle (width * height).

    class Rectangle {
      // your code here
    }
    
    let rect = new Rectangle(10, 5);
    console.log(rect.area); // 50
    
    rect.width = 20;
    console.log(rect.area); // 100
    
    rect.height = 12;
    console.log(rect.area); // 240
    
    try {
      rect.width = 0;
    } catch (e) {
      console.log(e); // RangeError: width must be positive
    }
    
    try {
      rect.height = -10;
    } catch (e) {
      console.log(e); // RangeError: height must be positive
    }
    

    Solution

    class Rectangle {
      #width;
      #height;
    
      constructor(width, height) {
        this.width = width;
        this.height = height;
      }
    
      get width() {
        return this.#width;
      }
    
      set width(value) {
        if (value > 0) {
          this.#width = value;
        } else {
          throw new RangeError('width must be positive');
        }
      }
    
      get height() {
        return this.#height;
      }
    
      set height(value) {
        if (value > 0) {
          this.#height = value;
        } else {
          throw new RangeError('height must be positive');
        }
      }
    
      get area() {
        return this.width * this.height;
      }
    }
    
    // test code omitted for brevity
    
  5. Create a MathUtils class with static methods add, subtract, multiply, and divide. These methods should perform basic arithmetic operations.

    class MathUtils {
      // your code here
    }
    
    console.log(MathUtils.add(5, 3));       // 8
    console.log(MathUtils.subtract(10, 4)); // 6
    console.log(MathUtils.multiply(6, 7));  // 42
    console.log(MathUtils.divide(20, 5));   // 4
    console.log(MathUtils.divide(10, 0));   // RangeError: Division by zero
    

    Solution

    class MathUtils {
      //highlight
      static add(a, b) {
        return a + b;
      }
    
      static subtract(a, b) {
        return a - b;
      }
    
      static multiply(a, b) {
        return a * b;
      }
    
      static divide(a, b) {
        if (b === 0) {
          throw new RangeError('Division by zero');
        }
        return a / b;
      }
      //endhighlight
    }
    
    console.log(MathUtils.add(5, 3));       // 8
    console.log(MathUtils.subtract(10, 4)); // 6
    console.log(MathUtils.multiply(6, 7));  // 42
    console.log(MathUtils.divide(20, 5));   // 4
    console.log(MathUtils.divide(10, 0));   // RangeError: Division by zero