ponyfoo.com

Practical Modern JavaScript

Dive into ES6 and the future of JavaScript — Modular JavaScript Book Series
O’Reilly Media334 PagesISBN 978-1-4919-4353-3

“I am delighted to support Nicolás’ endeavor because his book looks exactly like what people who are coming to JavaScript with fresh eyes need.”

– Brendan Eich

Ideal for professional software developers with a basic understanding of JavaScript, this practical book shows you how to build small, interconnected ES6 JavaScript modules that emphasize reusability. You’ll learn how to face a project with a modular mindset, and how to organize your applications into simple pieces that work well in isolation and can be combined to create a large, robust application.

This book focuses on two aspects of JavaScript development: modularity and ES6 features. You’ll learn how to tackle application development by following a scale-out approach. As pieces of your codebase grow too big, you can break them up into smaller modules.

The book can be read online for free or purchased through Amazon.

This book is part of the Modular JavaScript series.

🗞 Start with the book series launch announcement on Pony Foo
💳 Participate in the crowdfunding campaign on Indiegogo
🌩 Amplify the announcement on social media via Thunderclap
🐤 Share a message on Twitter or within your social circles
👏 Contribute to the source code repository on GitHub
🦄 Read the free HTML version of the book on Pony Foo
📓 Purchase the book from O’Reilly on Amazon

Chapter 3

Classes, Symbols, Objects, and Decorators

Now that we’ve covered the basic improvements to the syntax, we’re in good shape to take aim at a few other additions to the language: classes and symbols. Classes provide syntax to represent prototypal inheritance under the traditional class-based programming paradigm. Symbols are a new primitive value type in JavaScript, like strings, Booleans, and numbers. They can be used for defining protocols, and in this chapter we’ll investigate what that means. When we’re done with classes and symbols, we’ll discuss a few new static methods added to the Object built-in in ES6.

Classes

JavaScript is a prototype-based language, and classes are mostly syntactic sugar on top of prototypal inheritance. The fundamental difference between prototypal inheritance and classes is that classes can extend other classes, making it possible for us to extend the Array built-in—something that was very convoluted before ES6.

The class keyword acts, then, as a device that makes JavaScript more inviting to programmers coming from other paradigms, who might not be all that familiar with prototype chains.

Class Fundamentals

When learning about new language features, it’s always a good idea to look at existing constructs first, and then see how the new feature improves those use cases. We’ll start by looking at a simple prototype-based JavaScript constructor and then compare that with the newer classes syntax in ES6.

The following code snippet represents a fruit using a constructor function and adding a couple of methods to the prototype. The constructor function takes a name and the amount of calories for a fruit, and defaults to the fruit being in a single piece. There’s a .chop method that will slice another piece of fruit, and then there’s a .bite method. The person passed into .bite will eat a piece of fruit, getting satiety equal to the remaining calories divided by the amount of fruit pieces left.

function Fruit(name, calories) {
  this.name = name
  this.calories = calories
  this.pieces = 1
}
Fruit.prototype.chop = function () {
  this.pieces++
}
Fruit.prototype.bite = function (person) {
  if (this.pieces < 1) {
    return
  }
  const calories = this.calories / this.pieces
  person.satiety += calories
  this.calories -= calories
  this.pieces--
}

While fairly simple, the piece of code we just put together should be enough to note a few things. We have a constructor function that takes a couple of parameters, a pair of methods, and a number of properties. The next snippet codifies how one should create a Fruit and a person that chops the fruit into four slices and then takes three bites.

const person = { satiety: 0 }
const apple = new Fruit('apple', 140)
apple.chop()
apple.chop()
apple.chop()
apple.bite(person)
apple.bite(person)
apple.bite(person)
console.log(person.satiety)
// <- 105
console.log(apple.pieces)
// <- 1
console.log(apple.calories)
// <- 35

When using class syntax, as shown in the following code listing, the constructor function is declared as an explicit member of the Fruit class, and methods follow the object literal method definition syntax. When we compare the class syntax with the prototype-based syntax, you’ll notice we’re reducing the amount of boilerplate code quite a bit by avoiding explicit references to Fruit.prototype while declaring methods. The fact that the entire declaration is kept inside the class block also helps the reader understand the scope of this piece of code, making our classes’ intent clearer. Lastly, having the constructor explicitly as a method member of Fruit makes the class syntax easier to understand when compared with the prototype-based flavor of class syntax.

class Fruit {
  constructor(name, calories) {
    this.name = name
    this.calories = calories
    this.pieces = 1
  }
  chop() {
    this.pieces++
  }
  bite(person) {
    if (this.pieces < 1) {
      return
    }
    const calories = this.calories / this.pieces
    person.satiety += calories
    this.calories -= calories
    this.pieces--
  }
}

A not-so-minor detail you might have missed is that there aren’t any commas in between method declarations of the Fruit class. That’s not a mistake our copious copyeditors missed, but rather part of the class syntax. The distinction can help avoid mistakes where we treat plain objects and classes as interchangeable even though they’re not, and at the same time it makes classes better suited for future improvements to the syntax such as public and private class fields.

The class-based solution is equivalent to the prototype-based piece of code we wrote earlier. Consuming a fruit wouldn’t change in the slightest; the API for Fruit remains unchanged. The previous piece of code where we instantiated an apple, chopped it into smaller pieces, and ate most of it would work well with our class-flavored Fruit as well.

It’s worth noting that class declarations aren’t hoisted to the top of their scope, unlike function declarations. That means you won’t be able to instantiate, or otherwise access, a class before its declaration is reached and executed.

new Person() // <- ReferenceError: Person is not defined
class Person {
}

Besides the class declaration syntax presented earlier, classes can also be declared as expressions, just like with function declarations and function expressions. You may omit the name for a class expression, as shown in the following bit of code.

const Person = class {
  constructor(name) {
    this.name = name
  }
}

Class expressions could be easily returned from a function, making it possible to create a factory of classes with minimal effort. In the following example we create a JakePerson class dynamically in an arrow function that takes a name parameter and then feeds that to the parent Person constructor via super().

const createPersonClass = name => class extends Person {
  constructor() {
    super(name)
  }
}
const JakePerson = createPersonClass('Jake')
const jake = new JakePerson()

We’ll dig deeper into class inheritance later. Let’s take a more nuanced look at properties and methods first.

Properties and Methods in Classes

It should be noted that the constructor method declaration is an optional member of a class declaration. The following bit of code shows an entirely valid class declaration that’s comparable to an empty constructor function by the same name.

class Fruit {
}
function Fruit() {
}

Any arguments passed to new Log() will be received as parameters to the constructor method for Log, as depicted next. You can use those parameters to initialize instances of the class.

class Log {
  constructor(...args) {
    console.log(args)
  }
}
new Log('a', 'b', 'c')
// <- ['a' 'b' 'c']

The following example shows a class where we create and initialize an instance property named count upon construction of each instance. The get next method declaration indicates instances of our Counter class will have a next property that will return the results of calling its method, whenever that property is accessed.

class Counter {
  constructor(start) {
    this.count = start
  }
  get next() {
    return this.count++
  }
}

In this case, you could consume the Counter class as shown in the next snippet. Each time the .next property is accessed, the count raises by one. While mildly useful, this sort of use case is usually better suited by methods than by magical get property accessors, and we need to be careful not to abuse property accessors, as consuming an object that abuses of accessors may become very confusing.

const counter = new Counter(2)
console.log(counter.next)
// <- 2
console.log(counter.next)
// <- 3
console.log(counter.next)
// <- 4

When paired with setters, though, accessors may provide an interesting bridge between an object and its underlying data store. Consider the following example where we define a class that can be used to store and retrieve JSON data from localStorage using the provided storage key.

class LocalStorage {
  constructor(key) {
    this.key = key
  }
  get data() {
    return JSON.parse(localStorage.getItem(this.key))
  }
  set data(data) {
    localStorage.setItem(this.key, JSON.stringify(data))
  }
}

Then you could use the LocalStorage class as shown in the next example. Any value that’s assigned to ls.data will be converted to its JSON object string representation and stored in localStorage. Then, when the property is read from, the same key will be used to retrieve the previously stored contents, parse them as JSON into an object, and returned.

const ls = new LocalStorage('groceries')
ls.data = ['apples', 'bananas', 'grapes']
console.log(ls.data)
// <- ['apples', 'bananas', 'grapes']

Besides getters and setters, you can also define regular instance methods, as we’ve explored earlier when creating the Fruit class. The following code example creates a Person class that’s able to eat Fruit instances as we had declared them earlier. We then instantiate a fruit and a person, and have the person eat the fruit. The person ends up with a satiety level equal to 40, because he ate the whole fruit.

class Person {
  constructor() {
    this.satiety = 0
  }
  eat(fruit) {
    while (fruit.pieces > 0) {
      fruit.bite(this)
    }
  }
}
const plum = new Fruit('plum', 40)
const person = new Person()
person.eat(plum)
console.log(person.satiety)
// <- 40

Sometimes it’s necessary to add static methods at the class level, rather than members at the instance level. Using syntax available before ES6, instance members have to be explicitly added to the prototype chain. Meanwhile, static methods should be added to the constructor directly.

function Person() {
  this.hunger = 100
}
Person.prototype.eat = function () {
  this.hunger--
}
Person.isPerson = function (person) {
  return person instanceof Person
}

JavaScript classes allow you to define static methods like Person.isPerson using the static keyword, much like you would use get or set as a prefix to a method definition that’s a getter or a setter.

The following example defines a MathHelper class with a static sum method that’s able to calculate the sum of all numbers passed to it in a function call, by taking advantage of the Array#reduce method.

class MathHelper {
  static sum(...numbers) {
    return numbers.reduce((a, b) => a + b)
  }
}
console.log(MathHelper.sum(1, 2, 3, 4, 5))
// <- 15

Finally, it’s worth mentioning that you could also declare static property accessors, such as getters or setters (static get, static set). These might come in handy when maintaining global configuration state for a class, or when a class is used under a singleton pattern. Of course, you’re probably better off using plain old JavaScript objects at that point, rather than creating a class you never intend to instantiate or only intend to instantiate once. This is JavaScript, a highly dynamic language, after all.

1
Workers are a way of executing background tasks in browsers. The initiator can communicate with their workers, which run in a different execution context, via messaging.
2
You can find the proposal draft at GitHub.
3
in the works and you can find the draft online at GitHub.
4
Accessing properties via [] notation is disallowed due to the difficulty it would present when disambiguating grammar at the compiler level.
5
Ultima Online is a decades-old fantasy role playing game based on the Ultima universe.
6
The RunUO Git repository has the definition of CommandPropertyAttribute for RunUO.
7
Its use is widespread throughout the codebase, marking over 200 properties in the RunUO core alone.
8
You can find quite a few usage examples of the CommandProperty attribute in the PlayerMobile.cs class.
9
Reflection around JavaScript decorators is not being considered for JavaScript at this time, as it’d involve engines keeping more metadata in memory. We can, however, use symbols and lists to get around the need for native reflection.
Unlock with one Tweet!
Grants you full online access to Practical Modern JavaScript!
You can also read the book on the public git repository, but it won’t be as pretty! 😅