Understanding ES6 Classes in JavaScript

Introduction to Classes

Before ES6 (ECMAScript 2015), JavaScript primarily used constructor functions and prototypes for object creation and inheritance. ES6 introduced classes as syntactical sugar on top of the existing prototype-based inheritance model. Although classes look more like classes in languages such as Java or C++, they still rely on prototypes under the hood.

Defining a Class

A class in JavaScript is defined using the class keyword. Unlike function declarations, class declarations are not hoisted. You must define a class before you can instantiate it.

Syntax:

class ClassName {

  // class body

}

The Constructor Method

When you create a class, you can define a special method called constructor(). The constructor is automatically called when you create a new instance of a class using new. It’s typically used to initialize instance properties.

Example:

class Person {

  constructor(name, age) {

    this.name = name;

    this.age = age;

  }

  greet() {

    console.log(`Hello! My name is ${this.name} and I am ${this.age} years old.`);

  }

}

let alice = new Person(‘Alice’, 30);

alice.greet(); // “Hello! My name is Alice and I am 30 years old.”

Methods in Classes

Methods inside classes are defined without the function keyword. They become properties of the class’s prototype. Instances share these methods, rather than each having its own copy.

Example:

class Rectangle {

  constructor(width, height) {

    this.width = width;

    this.height = height;

  }

  area() {

    return this.width * this.height;

  }

}

let rect = new Rectangle(5, 10);

console.log(rect.area()); // 50

Static Methods and Properties

You can use the static keyword to define methods (and properties in newer versions of JavaScript) that belong to the class itself, not to instances. Static methods are often utility functions related to the class’s logic but not related to an individual instance.

Example:

class MathUtils {

  static add(a, b) {

    return a + b;

  }

}

console.log(MathUtils.add(2, 3)); // 5

// You can’t call mathUtilsInstance.add(2,3) if mathUtilsInstance is an instance

Computed Method Names

You can use computed property names in class method definitions by using []:

let methodName = ‘dynamicMethod’;

class Example {

  [methodName]() {

    console.log(“This method name was computed at runtime!”);

  }

}

let ex = new Example();

ex.dynamicMethod(); // “This method name was computed at runtime!”

Class Expressions

Classes can also be defined using expressions (anonymous or named):

const PersonClass = class {

  constructor(name) {

    this.name = name;

  }

  speak() {

    console.log(`Hi, I’m ${this.name}`);

  }

};

let bob = new PersonClass(‘Bob’);

bob.speak(); // “Hi, I’m Bob”

Inheritance with extends and super

ES6 classes make inheritance more straightforward. The extends keyword sets up the prototype chain so that a derived class inherits from a base class. The derived class’s constructor must call super() before using this.

Example:

class Animal {

  constructor(name) {

    this.name = name;

  }

  speak() {

    console.log(`${this.name} makes a noise.`);

  }

}

class Dog extends Animal {

  constructor(name, breed) {

    super(name); // call the parent constructor

    this.breed = breed;

  }

  speak() {

    console.log(`${this.name} barks. ${this.breed} is excited!`);

  }

}

let rufus = new Dog(‘Rufus’, ‘Labrador’);

rufus.speak(); // “Rufus barks. Labrador is excited!”

Overriding Methods

Just as shown above, the derived class can define a method with the same name as the base class method, effectively overriding it.

Calling Parent Methods

To call the parent’s method inside a derived class’s method, use super.methodName():

class Parent {

  greet() {

    console.log(“Hello from Parent”);

  }

}

class Child extends Parent {

  greet() {

    super.greet(); // calls Parent.greet()

    console.log(“Hello from Child”);

  }

}

let c = new Child();

c.greet();

// “Hello from Parent”

// “Hello from Child”

Important Points

  • Classes are just syntactic sugar over prototypes.
  • Classes cannot be called without new.
  • Class declarations are not hoisted; you must define them before usage.
  • The super keyword is used both for calling the parent’s constructor and parent methods.
  • Fields (class properties declared inside the class body) were added in later versions of JavaScript and may need certain language features or transpilation for cross-browser support.

Multiple Choice Questions 

  1. What is the primary purpose of the class keyword in JavaScript (ES6)?
    A. To introduce classical inheritance similar to Java or C++.
    B. To provide a syntactical sugar over prototype-based inheritance.
    C. To replace all functions.
    D. To create private variables automatically.
    Answer: B
    Explanation: ES6 classes are syntactical sugar over JavaScript’s existing prototype-based inheritance system.
  2. Which of the following is true about the constructor method in a class?
    A. It is called automatically when the class definition is read.
    B. It is called when a new instance of the class is created with new.
    C. It can only be defined once per class using the keyword construct.
    D. It is optional and never called if omitted.
    Answer: B
    Explanation: The constructor is invoked each time you create a new instance with new.
  3. What happens if you do not define a constructor in a class?
    A. It fails to instantiate objects.
    B. A default empty constructor is used.
    C. No instances can be created from that class.
    D. The class cannot extend another class.
    Answer: B
    Explanation: If no constructor is defined, a default constructor is used that basically returns this.
  4. How do you define a static method in a class?
    A. By prefixing the method name with class.
    B. By putting the method inside the constructor.
    C. By using the static keyword before the method name.
    D. By defining the method outside of the class.
    Answer: C
    Explanation: The static keyword makes a method a class-level method, not tied to instances.
  5. What is required when a subclass defines a constructor?
    A. It must call super() before using this.
    B. It must not have any methods.
    C. It must not call super() at all.
    D. It can only define properties after the constructor ends.
    Answer: A
    Explanation: In a subclass, super() must be called before this is accessed in the constructor.

Consider:

class Animal {

  constructor(name) {

    this.name = name;

  }

  speak() {

    console.log(`${this.name} makes a noise.`);

  }

}

class Dog extends Animal {

  speak() {

    console.log(`${this.name} barks.`);

  }

}

let r = new Dog(‘Rex’);

r.speak();

  1. What is the output?
    A. “Rex makes a noise.”
    B. “Rex barks.”
    C. Error: must call super() in the Dog constructor.
    D. Nothing.
    Answer: B
    Explanation: Dog overrides speak() and prints “Rex barks.”
  2. Which statement about classes is false?
    A. Classes are not hoisted.
    B. You cannot invoke a class without new.
    C. Class bodies are executed in strict mode.
    D. Class methods automatically bind this to the instance.
    Answer: D
    Explanation: Class methods do not automatically bind this. You must do so manually if needed.
  3. What does super() do inside a subclass constructor?
    A. Calls the parent class’s constructor.
    B. Calls a global function named super.
    C. Declares a super variable.
    D. Nothing special.
    Answer: A
    Explanation: super() invokes the parent class’s constructor.
  4. Which keyword is used to create a subclass from a parent class?
    A. derive
    B. subclass
    C. extend
    D. extends
    Answer: D
    Explanation: The extends keyword is used for inheritance.
  5. How do you properly call a parent method from a child method with the same name?
    A. super.methodName()
    B. this.parent.methodName()
    C. parentClassName.methodName()
    D. this.super.methodName()
    Answer: A
    Explanation: Use super.methodName() inside the child class to call the parent’s method.
  6. If a method is defined as static in a class, how do you call it?
    A. By creating an instance and then calling the method on that instance.
    B. Directly on the class itself.
    C. Using super.staticMethodName() always.
    D. You cannot call static methods.
    Answer: B
    Explanation: Static methods are called on the class itself, e.g., MyClass.myStaticMethod().
  7. Which of the following best describes class fields (public instance fields)?
    A. They are properties defined inside the constructor.
    B. They are a new addition allowing you to define instance properties at the top-level of the class body.
    C. They automatically become private.
    D. They must be defined outside the class.
    Answer: B
    Explanation: Public class fields allow defining instance properties without placing them in the constructor.
  8. Can you declare private fields in a class, and if so, how?
    A. Yes, by using private keyword before the field name.
    B. Yes, by prefixing the field name with #.
    C. No, ES6 classes do not support private fields.
    D. Yes, by defining them inside a closure.
    Answer: B
    Explanation: Private fields are declared with a # prefix as per newer specifications (#myField).
  9. What happens if you try to extend a non-constructor object?
    A. The code throws a TypeError.
    B. The class silently fails.
    C. The child class just behaves like a regular class without parent.
    D. It creates a new empty parent class.
    Answer: A
    Explanation: class X extends Y where Y is not a constructor or null will throw a TypeError.
  10. Which is a correct way to define a class expression?
    A. let MyClass = class { constructor() {} };
    B. class() {}
    C. class { constructor() {}; } as a statement by itself.
    D. function class() {}
    Answer: A
    Explanation: You can define an anonymous class and assign it to a variable.
  11. Which statement is true about the constructor in an ES6 class?
    A. Every class must define a constructor.
    B. If omitted, a default constructor is created automatically.
    C. You can define multiple constructors in a single class.
    D. The constructor cannot call super().
    Answer: B
    Explanation: A default constructor (…args) => { super(…args); } is provided if not specified, in the case of inheritance, or an empty one if no inheritance.
  12. If you have class A {} and class B extends A {}, what is the prototype chain of an instance of B?
    A. b → B.prototype → Object.prototype
    B. b → B.prototype → A.prototype → Object.prototype
    C. b → A.prototype → B.prototype → Object.prototype
    D. b → Object.prototype
    Answer: B
    Explanation: Instances of B inherit from B.prototype, which inherits from A.prototype, which inherits from Object.prototype.
  13. Which of these is not allowed inside a class?
    A. Defining methods.
    B. Defining static methods.
    C. Defining private fields with #.
    D. Executing code outside of a method (top-level code directly inside the class body).
    Answer: D
    Explanation: The class body only allows method definitions, fields, and static fields. No arbitrary statements are allowed.
  14. How do you add getters and setters in a class?
    A. By defining methods prefixed with get and set keywords.
    B. By using function get property() {} inside the class.
    C. By using Object.defineProperty() inside the constructor.
    D. By defining a static method called get.
    Answer: A
    Explanation: You can define getters and setters using get propertyName() and set propertyName(value) syntax inside the class.
  15. Classes in JavaScript run in which mode by default?
    A. Sloppy mode.
    B. Strict mode.
    C. Debug mode.
    D. Strict mode only for static methods.
    Answer: B
    Explanation: All code inside a class body is executed in strict mode.

10 Coding Exercises with Full Solutions and Explanations

Exercise 1:
Task: Create a Car class that takes model and year in the constructor and has a describe() method that logs these values.
Solution:

class Car {

  constructor(model, year) {

    this.model = model;

    this.year = year;

  }

  describe() {

    console.log(`This car is a ${this.model} from ${this.year}.`);

  }

}

const myCar = new Car(‘Toyota Camry’, 2020);

myCar.describe(); // “This car is a Toyota Camry from 2020.”

Explanation: We defined a class with a constructor and a method. Instances inherit the describe() method.

Exercise 2:
Task: Add a static method isCar(obj) to the Car class that returns true if obj is an instance of Car, otherwise false.
Solution:

class Car {

  constructor(model) {

    this.model = model;

  }

  static isCar(obj) {

    return obj instanceof Car;

  }

}

const myCar = new Car(‘Honda Accord’);

console.log(Car.isCar(myCar)); // true

console.log(Car.isCar({model: ‘Fake Car’})); // false

Explanation: Static methods are called on the class, not on instances.

Exercise 3:
Task: Create a base class Shape with a name property, and a derived class Circle that extends Shape. Circle should take name and radius in its constructor and have a method area() that returns Math.PI * radius^2.
Solution:

class Shape {

  constructor(name) {

    this.name = name;

  }

}

class Circle extends Shape {

  constructor(name, radius) {

    super(name);

    this.radius = radius;

  }

  area() {

    return Math.PI * this.radius * this.radius;

  }

}

const c = new Circle(‘MyCircle’, 5);

console.log(c.area()); // 78.53981633974483

Explanation: Circle inherits from Shape, calls super(name) to initialize the parent part, and defines its own methods.

Exercise 4:
Task: Override a method. Create a Parent class with a method greet(), and a Child class that extends it and overrides greet() by first calling super.greet() and then logging another message.
Solution:

class Parent {

  greet() {

    console.log(“Hello from Parent”);

  }

}

class Child extends Parent {

  greet() {

    super.greet();

    console.log(“Hello from Child”);

  }

}

const kid = new Child();

kid.greet();

// “Hello from Parent”

// “Hello from Child”

Explanation: super.greet() calls the parent class’s greet() method, then Child adds its own message.

Exercise 5:
Task: Use getters and setters. Create a Person class with a firstName and lastName. Add a getter fullName that returns the full name, and a setter fullName that splits a string into first and last name.
Solution:

class Person {

  constructor(firstName, lastName) {

    this.firstName = firstName;

    this.lastName = lastName;

  }

  get fullName() {

    return `${this.firstName} ${this.lastName}`;

  }

  set fullName(name) {

    const [first, last] = name.split(‘ ‘);

    this.firstName = first;

    this.lastName = last;

  }

}

const p = new Person(‘Alice’, ‘Smith’);

console.log(p.fullName); // “Alice Smith”

p.fullName = ‘Mary Johnson’;

console.log(p.firstName); // “Mary”

console.log(p.lastName);  // “Johnson”

Explanation: Getters and setters provide a nice syntax for retrieving and updating internal properties.

Exercise 6:
Task: Create a class Counter with a static property count = 0. Each time you create a new instance of Counter, increment count. Show the value of Counter.count after creating several instances.
Solution:

class Counter {

  static count = 0;

  constructor() {

    Counter.count++;

  }

}

new Counter();

new Counter();

new Counter();

console.log(Counter.count); // 3

Explanation: Static class fields can store data at the class level. Each new instance increments the static count.

Exercise 7:
Task: Demonstrate private fields. Create a class BankAccount with a private field #balance. Add a method deposit(amount) and withdraw(amount) that adjust #balance if valid. Add a getBalance() method that returns the current balance.
(Note: Private fields # may not be supported in all runtimes without transpilation. Assuming a modern environment.)

Solution:

class BankAccount {

  #balance = 0;

  deposit(amount) {

    if (amount > 0) this.#balance += amount;

  }

  withdraw(amount) {

    if (amount > 0 && amount <= this.#balance) {

      this.#balance -= amount;

    }

  }

  getBalance() {

    return this.#balance;

  }

}

const account = new BankAccount();

account.deposit(100);

account.withdraw(30);

console.log(account.getBalance()); // 70

// console.log(account.#balance); // Syntax error: private field cannot be accessed outside class

Explanation: The #balance field is private. It cannot be accessed outside the class.

Exercise 8:
Task: Class Expression: Create a class using a class expression and instantiate it.
Solution:

const AnimalClass = class {

  constructor(type) {

    this.type = type;

  }

  speak() {

    console.log(`${this.type} makes a sound.`);

  }

};

const cat = new AnimalClass(‘Cat’);

cat.speak(); // “Cat makes a sound.”

Explanation: Class expressions are just like class declarations but assigned to a variable.

Exercise 9:
Task: In a class hierarchy, call a parent method from a child. Use super.speak() in a Bird class that extends Animal, where both have a speak() method.
Solution:

class Animal {

  constructor(name) {

    this.name = name;

  }

  speak() {

    console.log(`${this.name} makes a generic animal sound.`);

  }

}

class Bird extends Animal {

  speak() {

    super.speak();

    console.log(`${this.name} chirps.`);

  }

}

const parrot = new Bird(‘Polly’);

parrot.speak();

// “Polly makes a generic animal sound.”

// “Polly chirps.”

Explanation: We override speak() in Bird and still access the parent’s version via super.speak().

Exercise 10:
Task: Create a class User with a constructor that takes username. Add a method login() that prints a message. Create a subclass Admin that extends User and adds an adminLevel property. Override login() in Admin to print a more specialized message.

Solution:

class User {

  constructor(username) {

    this.username = username;

  }

  login() {

    console.log(`${this.username} is logged in.`);

  }

}

class Admin extends User {

  constructor(username, adminLevel) {

    super(username);

    this.adminLevel = adminLevel;

  }

  login() {

    console.log(`Admin ${this.username} with level ${this.adminLevel} is logged in.`);

  }

}

const user = new User(‘regularUser’);

user.login(); // “regularUser is logged in.”

const admin = new Admin(‘superAdmin’, 10);

admin.login(); // “Admin superAdmin with level 10 is logged in.”

Explanation: The Admin class inherits from User and overrides the login() method. The Admin constructor calls super() to initialize the username from User.

Summary

ES6 classes provide a more familiar and cleaner syntax for creating and managing objects, constructors, and inheritance in JavaScript. Under the hood, classes still use prototypes and provide syntactic sugar rather than fundamentally changing how JavaScript’s inheritance model works. By understanding classes, constructors, inheritance via extends, and the super keyword, you can write more organized and maintainable object-oriented JavaScript code.