blog

  • Home
  • blog
  • Object-oriented JavaScript: A Deep Dive into ES6 Classes

Object-oriented JavaScript: A Deep Dive into ES6 Classes

Often we need to represent an idea or concept in our programs — maybe a car engine, a computer file, a router, or a temperature reading. Representing these concepts directly in code comes in two parts: data to represent the state, and functions to represent the behavior. ES6 classes give us a convenient syntax for defining the state and behavior of objects that will represent our concepts.

ES6 classes make our code safer by guaranteeing that an initialization function will be called, and they make it easier to define a fixed set of functions that operate on that data and maintain valid state. If you can think of something as a separate entity, it’s likely you should define a class to represent that “thing” in your program.

Consider this non-class code. How many errors can you find? How would you fix them?

// set today to December 24
const today = {
  month: 24,
  day: 12,
};

const tomorrow = {
  year: today.year,
  month: today.month,
  day: today.day + 1,
};

const dayAfterTomorrow = {
  year: tomorrow.year,
  month: tomorrow.month,
  day: tomorrow.day + 1 <= 31 ? tomorrow.day + 1 : 1,
};

The date today isn’t valid: there’s no month 24. Also, today isn’t fully initialized: it’s missing the year. It would be better if we had an initialization function that couldn’t be forgotten. Notice also that, when adding a day, we checked in one place if we went beyond 31 but missed that check in another place. It would be better if we interacted with the data only through a small and fixed set of functions that each maintain valid state.

Here’s the corrected version that uses classes.

class SimpleDate {
  constructor(year, month, day) {
    // Check that (year, month, day) is a valid date
    // ...

    // If it is, use it to initialize "this" date
    this._year = year;
    this._month = month;
    this._day = day;
  }

  addDays(nDays) {
    // Increase "this" date by n days
    // ...
  }

  getDay() {
    return this._day;
  }
}

// "today" is guaranteed to be valid and fully initialized
const today = new SimpleDate(2000, 2, 28);

// Manipulating data only through a fixed set of functions ensures we maintain valid state
today.addDays(1);
JARGON TIP:

  • When a function is associated with a class or object, we call it a
  • When an object is created from a class, that object is said to be an


For a high-quality, in-depth introduction to ES6, you can’t go past Canadian full-stack developer Wes Bos. Try his course here, and use the code SITEPOINT to get 25% off and to help support SitePoint.


Constructors

The constructor method is special, and it solves the first problem. Its job is to initialize an instance to a valid state, and it will be called automatically so we can’t forget to initialize our objects.

Keep Data Private

We try to design our classes so that their state is guaranteed to be valid. We provide a constructor that creates only valid values, and we design methods that also always leave behind only valid values. But as long as we leave the data of our classes accessible to everyone, someone will mess it up. We protect against this by keeping the data inaccessible except through the functions we supply.

JARGON TIP: Keeping data private to protect it is called encapsulation.

Privacy with Conventions

Unfortunately, private object properties don’t exist in JavaScript. We have to fake them. The most common way to do that is to adhere to a simple convention: if a property name is prefixed with an underscore (or, less commonly, suffixed with an underscore), then it should be treated as non-public. We used this approach in the earlier code example. Generally this simple convention works, but the data is technically still accessible to everyone, so we have to rely on our own discipline to do the right thing.

Privacy with Privileged Methods

The next most common way to fake private object properties is to use ordinary variables in the constructor, and capture them in closures. This trick gives us truly private data that’s inaccessible to the outside. But to make it work, our class’s methods would themselves need to be defined in the constructor and attached to the instance:

class SimpleDate {
  constructor(year, month, day) {
    // Check that (year, month, day) is a valid date
    // ...

    // If it is, use it to initialize "this" date's ordinary variables
    let _year = year;
    let _month = month;
    let _day = day;

    // Methods defined in the constructor capture variables in a closure
    this.addDays = function(nDays) {
      // Increase "this" date by n days
      // ...
    }

    this.getDay = function() {
      return _day;
    }
  }
}

Privacy with Symbols

Symbols are a new feature of JavaScript as of ES6, and they give us another way to fake private object properties. Instead of underscore property names, we could use unique symbol object keys, and our class can capture those keys in a closure. But there’s a leak. Another new feature of JavaScript is Object.getOwnPropertySymbols, and it allows the outside to access the symbol keys we tried to keep private:

const SimpleDate = (function() {
  const _yearKey = Symbol();
  const _monthKey = Symbol();
  const _dayKey = Symbol();

  class SimpleDate {
    constructor(year, month, day) {
      // Check that (year, month, day) is a valid date
      // ...

      // If it is, use it to initialize "this" date
      this[_yearKey] = year;
      this[_monthKey] = month;
      this[_dayKey] = day;
     }

    addDays(nDays) {
      // Increase "this" date by n days
      // ...
    }

    getDay() {
      return this[_dayKey];
    }
  }

  return SimpleDate;
}());

Privacy with Weak Maps

Weak maps are also a new feature of JavaScript. We can store private object properties in key/value pairs using our instance as the key, and our class can capture those key/value maps in a closure:

const SimpleDate = (function() {
  const _years = new WeakMap();
  const _months = new WeakMap();
  const _days = new WeakMap();

  class SimpleDate {
    constructor(year, month, day) {
      // Check that (year, month, day) is a valid date
      // ...

      // If it is, use it to initialize "this" date
      _years.set(this, year);
      _months.set(this, month);
      _days.set(this, day);
    }

    addDays(nDays) {
      // Increase "this" date by n days
      // ...
    }

    getDay() {
      return _days.get(this);
    }
  }

  return SimpleDate;
}());

Other Access Modifiers

There are other levels of visibility besides “private” that you’ll find in other languages, such as “protected”, “internal”, “package private”, or “friend”. JavaScript still doesn’t give us a way to enforce those other levels of visibility. If you need them, you’ll have to rely on conventions and self discipline.

Referring to the Current Object

Look again at getDay(). It doesn’t specify any parameters, so how does it know the object for which it was called? When a function is called as a method using the object.function notation, there’s an implicit argument that it uses to identify the object, and that implicit argument is assigned to an implicit parameter named this. To illustrate, here’s how we would send the object argument explicitly rather than implicitly:

// Get a reference to the "getDay" function
const getDay = SimpleDate.prototype.getDay;

getDay.call(today); // "this" will be "today"
getDay.call(tomorrow); // "this" will be "tomorrow"

tomorrow.getDay(); // same as last line, but "tomorrow" is passed implicitly

Continue reading %Object-oriented JavaScript: A Deep Dive into ES6 Classes%

LEAVE A REPLY