Lesson 27. Classes

After reading lesson 27, you will

  • Understand the syntax for defining classes
  • Know how to instantiate classes and how to use constructor functions
  • Know how to export classes from modules
  • Understand that class methods are not bound
  • Know how to assign class and static properties

Classes are little more than syntactic sugar for declaring a constructor function and setting its prototype. Even with the introduction of classes, JavaScript isn’t statically or strongly typed. It remains a dynamically and weakly typed language. However, classes do create a simple self-contained syntax for defining constructors with prototypes. The main advantage over constructors, though, besides syntax, is when extending classes, which we’ll get to in the next lesson. Without having an easily extendable built-in construct such as classes, many libraries like Backbone.js, React.js, and several others had to continue to reinvent the wheel to allow extending their base objects. This library-specific form of extending objects will become a thing of the past as more and more libraries start to provide a base JavaScript class that can easily be extended. This means once you learn how to use and extend classes, you’ll have a jump start on many of today’s and tomorrow’s frameworks.

Consider this

The following two functions are current JavaScript patterns of instantiating objects: the factory function and the constructor function. If you were to design a new syntax for instantiating objects, how would you improve it?

 const base = {
   accel() { /* go forwards */ },
   brake() { /* stop */ },
   reverse() { /* go backwards */ },
   honk() { /* be obnoxious */ }
 }

 function carFactory (make) {
   const car = Object.create(base);
   car.make = make;
   return car;
 }

 function CarConstructor(make) {
   this.make = make;
 }

CarConstructor.prototype = base;

27.1. Class declarations

Let’s say you’ll building a web application that needs to connect to several APIs for various resources, such as users, teams, and products. You may want to create several store objects that store the records for the various resources. You could create a store class like so:

class DataStore {
  // class body
}

Here you declared a class with the keyword class followed by the class name, DataStore. The name doesn’t have to be capitalized, but it is a convention to do so. After the name of the class, you have a pair of braces {}: the class body, all the methods and properties that make up the class, go between these two braces.

Let’s say, for example, that you need a method called fetch on your DataStore class to handle fetching the records from the database. You would add a method like so:

class DataStore {
  fetch() {
    // fetch records from data base
  }
}

Adding a method here is the exact same syntax as the shorthand method names on objects that you learned about in lesson 12. Don’t let this fool you, though: a class definition is not an object and other syntaxes won’t work. The attempt to create a method in the next listing will result in a syntax error.

Listing 27.1. Incorrect syntax for adding methods to classes
class DataStore {
  fetch: function() {                    1
    // fetch records from data base
  }
}

  • 1 Syntax Error

Class methods work like any other function in JavaScipt: they accept parameters and can use destructuring and default values. In fact, it’s not the syntax of the function here that causes the error, but the use of a colon (:). Colons are used to separate property names and property values on object literals, but a class declaration doesn’t use them. Also, any use of this inside a method will refer to the instance, not the class.

Another difference between the class syntax and object syntax is that there are no commas separating properties in a class like there are in an object:

class DataStore {
  fetch() {
    // fetch records from data base
  }                                    1
  update() {
    // update a record
  }
}

  • 1 No comma separating the two methods

Notice how there are no commas separating the class methods. If you were creating an object, you would need a comma or you would get a syntax error. But in a class declaration, the opposite is true in that adding commas will result in a syntax error.

Quick check 27.1

Q1:

Can you spot the two problems in the following car class?

 class Car {
   steer(degree) {
     // turn the car by degree
   },
   accel(amount=1) {
     // accelerate the car by amount
   }
   break: function() {
     // decelerate the car
   }
}

QC 27.1 answer

A1:

Commas and colons are not allowed.

 

27.2. Instantiating classes

Once you have your class definition, you can create an instance of it using the new keyword:

const dataStore = new DataStore();          1

  • 1 Create an instance of DataStore in a new constant named dataStore.

If you try to instantiate the class without the new keyword, you receive a TypeError:

const myStore = DataStore();          1

  • 1 Class constructor DataStore can’t be invoked without ‘new’

You can also pass arguments to your new class instance when creating an instance like so:

const userStore = new DataStore('users');

Of course, this raises the question: how does the class receive these parameters? There’s a special method name called constructor that gets invoked automatically when an instance is created. Any arguments given when creating a new instance will be passed to this constructor function.

class DataStore {
  constructor(resource) {
    // setup an api connection for the specified resource
  }
}

The constructor function gets invoked in the context of the instance that’s being created. That means that this inside of the constructor function will refer to the instance that’s being created, not the class itself. The constructor function is optional, and is provided as a hook to do any initial setup on instances of your classes. The placement of the constructor function doesn’t matter, but I like to keep mine at the top.

In the next section we’ll take a quick look at exporting classes from modules.

Quick check 27.2

Q1:

What will be logged when creating the following class?

 class Pet {
   constructor(species) {
     console.log(`created a pet ${species}`);
   }
 }

const myPet = new Pet('dog');

QC 27.2 answer

A1:

“created a pet dog”

 

27.3. Exporting classes

When creating JavaScript classes, it’s likely that you’ll do so in modules. That means that you’ll need to be able to export them. For example, if you wanted to export the DataStore class you created from a module, you could do it like this:

export default class DataStore {           1
  // class body
}

  • 1 Specify the DataStore class as the default export.

This exports the class as the default. Importing it is the same as importing any other default; you can use any name you desire:

import Store from './data_store'

Additionally you can omit the default and export the class by name:

export class DataStore {            1
  // class body
}

  • 1 Export the DataStore class under the name DataStore.

As you probably guessed, this exports the class as the name DataStore and it must be imported using its name:

import { DataStore } from './data_store'

In most cases you’ll likely want to only create one class per module and export it as the default.

Quick check 27.3

Q1:

Which of the following two class exports are valid?

export class A {
  // class body
}
export default class B {
 // class body
}

QC 27.3 answer

A1:

They’re both valid.

 

27.4. Class methods are not bound

Some developers new to classes may be surprised to find out that class methods are not automatically bound. Let’s say, for example, that the DataStore class uses an ajax library internally to handle the API calls. You may have something set up like the next listing.

Listing 27.2. Using unbound methods as callbacks
class DataStore {
  fetch() {
    ajax(this.url, this.handleNewRecords)         1
  }
  handleNewRecords(records) {
    this.records = records                        2
  }
}

  • 1 Making an ajax call and specifying this.handleNewRecords as the callback
  • 2 The keyword this will not point to the instance because it isn’t bound.

In listing 27.2 you’ll use an ajax library to fetch records for you. The ajax library takes two arguments: the URL to load data from, and a callback function to invoke with the data once loaded. This may seem like everything is fine, but since the handleNewRecords method isn’t bound, when it’s invoked from the ajax library, this will no longer point to the DataStore instance so the records won’t get properly stored.

There are a few different ways to fix this. The simplest is to use an arrow function as the callback:

class DataStore {
  fetch() {
    ajax(this.url, records => this.records = records)
  }
}

This will work just fine. But if your callback is more complex and is used in multiple places, this approach may not be suitable. You can still use an arrow function, but as an intermediary step:

class DataStore {
  fetch() {
    ajax(this.url, records => this.handleNewrecords(records) )
  }
  handleNewRecords(records) {
    // do something more complex like
    // merging new records in with existing records
  }
}

In the next lesson, you’ll learn one more way of binding methods when you learn about class properties.

Quick check 27.4

Q1:

Invoking car.delayedHonk() will result in an error. Why?

 class Car {
   honk() {
     this.honkAudio.play();
   }
   delayedHonk() {
     window.setTimeout(this.honk, 1000);
   }
 }

 const car = new Car();
car.delayedHonk();

QC 27.4 answer

A1:

Because this.honk is used as a callback but is not bound.

 

27.5. Setting instance properties in class definitions

There’s another aspect of classes that, at the time of this writing, is just a proposal but is gaining lots of use, and that’s setting instance properties in class definitions. Setting properties in classes (unlike methods) may seem simple at first, but actually has more to it than might initially meet the eye. This is because the methods are added to the prototype, but the properties are assigned to each instance.

Let’s assume you want to set up a property on your DataStore to determine the URL of the resource. You also want an array property to store the actual records in. You could do so as shown in the following listing.

Listing 27.3. Setting properties on classes
class DataStore {
  url = '/data/resources';                   1
  records = [];                              2

  fetch() {
    ajax(this.url, records => this.records = records)
  }
}
console.log(DataStore.url);                  3

const store = new DataStore();
console.log(store.url);                      4
console.log(store.records.length);           5

  • 1 A property named url
  • 2 A property named records
  • 3 undefined
  • 4 “/data/resources”
  • 5 0

The properties, as you can see, will be available on the instance that gets created, not the class itself. Notice how class properties look like an assignment directly in the class definition. This is because they technically are an assignment. Unlike class methods, class properties are a syntactic sugar that gets rewritten directly in the constructor function. That means the url and records assignments you just set would actually end up being as if you had made them inside the constructor function:

class DataStore {
  constructor() {
    this.url = '/data/resources';
    this.records = [];
  }
}

This is different from class methods, because those get added to the class’s prototype. The instances then inherit those methods via prototypical inheritance. But the class properties don’t get set on the prototype; they end up being set directly on the instance itself. Setting properties directly on the prototype would lead to bugs. Let’s explore why in an example:

class DataStore {};

DataStore.prototype.records = [];

const storeA = new DataStore();

console.log(storeA.records.length);          1

const storeB = new DataStore();

console.log(storeB.records.length);          1

storeB.records.push('Example Record')

console.log(storeA.records.length);          2
console.log(storeA.records[0]);              3

  • 1 0
  • 2 1
  • 3 ‘Example Record’

Here you set the records property directly on the DataStore’s prototype instead of the instance. This means all instances will use the same records array because it’s set on their prototype, not on the instance objects themselves. When object storeB adds a record, storeA receives a new record as well. They’re sharing the same records array!

One obvious benefit of class instance properties is declaring bound methods. In the last section, you were running into some issues because your handleNewRecords method wasn’t bound to the instance. You could make it bound by declaring it as a property pointing to an arrow function:

class DataStore {
  fetch() {
    ajax(this.url, this.handleNewrecords)
  }
  handleNewRecords = (records) => {
    // do something more complex like
    // merging new records in with existing records
  }
}

In the next section we’ll take a look at another type of class property, static properties.

Quick check 27.5

Q1:

The following class suffers from a syntax mistake common with class properties. Can you spot it?

 class Nachos {
   toppings: ['cheese', 'jalapenos']
}

QC 27.5 answer

A1:

It should be

toppings = ['cheese', 'jalapenos'];

 

27.6. Static properties

Static properties on classes are a special type of property that doesn’t set a property on the instance or even the prototype, but on the class object (the constructor) itself. Static properties make sense for properties that won’t change across instances. For example, your DataStore could potentially have a static property for what domain to use when connecting to APIs:

class DataStore {
  static domain = 'https://example.com';       1
  static url(path) {                           2
    return `${this.domain}${path}`
  }
  constructor(resource) {
    this.url = DataStore.url(resource);        3
  }
}

const userStore = new DataStore('/users');
console.log(userStore.url);                    4

  • 1 Assigning a static property
  • 2 Setting a static method
  • 3 Invoking the static method
  • 4 “https://example.com/user

You use a static property domain and a static method `url`[1] to generate the instance’s URL from the specified resource in the constructor function.

1

While static properties are at the time of this writing still just a proposal, static methods were introduced in ES2015.

Static properties are just syntactic sugar for assigning them directly on the classes themselves, as shown in the next listing.

Listing 27.4. Desugaring static properties
class DataStore {
  constructor(resource) {
    this.url = DataStore.url(resource);
  }
}

DataStore.domain = 'https://example.com';
DataStore.url = function(path) {
  return `${this.domain}${path}`
}

That sums up creating and using classes, but we aren’t done with classes yet. In the next lesson we’ll take a look at extending classes.

Quick check 27.6

Q1:

Which of the following console.logs actually logs the number of wheels?

class Bicycle {
  static numberOfWheels = 2;
}
const bike = new Bicycle();

console.log(bike.numberOfWheels);
console.log(Bicycle.prototype.numberOfWheels);
console.log(Bicycle.numberOfWheels);

QC 27.6 answer

A1:

console.log(Bicycle.numberOfWheels);

 

Summary

In this lesson you learned how to define and use your own classes.

  • Classes are created with the class keyword followed by the class name and class body.
  • Class methods use the shorthand method syntax for declaring methods.
  • Classes don’t support colons for declaring properties or methods.
  • Classes shouldn’t have commas separating methods or properties.
  • The constructor function is executed when the class is instantiated.
  • Class methods aren’t automatically bound to the instance.

Let’s see if you got this:

Q27.1

Take the following constructor function and prototype and convert it into a class:

function Fish(name) {
  this.name = name;
  this.hunger = 1;
  this.dead = false;
  this.born = new Date();
}
Fish.prototype = {
  eat(amount=1) {
    if (this.dead) {
      console.log(`${this.name} is dead and can no longer eat.`);
      return;
    }
    this.hunger -= amount;
    if (this.hunger < 0) {
      this.dead = true;
      console.log(`${this.name} has died from over eating.`)
      return
    }
  },
  sleep() {
    this.hunger++;
    if (this.hunger >= 5) {
      this.dead = true;
      console.log(`${this.name} has starved.`)
    }
  },
  isHungry: function() {
    return this.hunger > 0;
  }
}

const oscar = new Fish('oscar');
console.assert(oscar instanceof Fish);
console.assert(oscar.isHungry());
while(oscar.isHungry()) {
  oscar.eat();
}
console.assert(!oscar.isHungry());
console.assert(!oscar.dead);
oscar.eat();
console.assert(oscar.dead);

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
52.14.17.40