[[classes-symbols-objects-and-decorators]]
== Classes, Symbols, Objects, pass:[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 ((("classes", id="class3")))is a prototype-based language, and classes are mostly syntactic sugar on top of prototypal inheritance. The fundamental difference between ((("prototypal inheritance")))((("classes", "versus prototypal inheritance", secondary-sortas="prototypal")))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 ((("classes", "fundamentals", id="class3f")))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 ((("classes", "class declaration syntax", id="class3cds")))represents a fruit using a ((("constructor()", id="cf3")))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.
[source,javascript]
----
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.
[source,javascript]
----
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 ((("constructor()", startref="cf3")))flavor of class syntax.
[source,javascript]
----
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 ((("classes", "commas in class syntax")))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.
[source,javascript]
----
new Person() // <- ReferenceError: Person is not defined
class Person {
}
----
Besides ((("classes", "class declaration syntax", startref="class3cds")))the class declaration syntax presented earlier, classes can also be declared as ((("classes", "expression syntax", id="class3es")))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.
[source,javascript]
----
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()`.
[source,javascript]
----
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 ((("classes", "fundamentals", startref="class3f")))first.
==== Properties and Methods in Classes
It ((("classes", "properties and methods in", id="class3pami")))((("properties", id="p3ic")))((("methods", id="m3ic")))should be noted that the `constructor` ((("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.
[source,javascript]
----
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.
[source,javascript]
----
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.
[source,javascript]
----
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 ((("getters")))magical `get` ((("accessors", id="acc3")))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.
[source,javascript]
----
const counter = new Counter(2)
console.log(counter.next)
// <- 2
console.log(counter.next)
// <- 3
console.log(counter.next)
// <- 4
----
When paired with ((("setters")))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`.
[source,javascript]
----
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.
[source,javascript]
----
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.
[source,javascript]
----
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.
[source,javascript]
----
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")))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.
[source,javascript]
----
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", startref="acc3")))accessors, such as ((("getters")))((("setters")))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, ((("classes", "properties and methods in", startref="class3pami")))((("properties", "in classes", startref="p3ic")))((("methods", "in classes", startref="m3ic")))after all.
==== Extending JavaScript Classes
You ((("classes", "extending", id="class3ext")))could use plain JavaScript to extend the `Fruit` class, but as you will notice by reading the next code snippet, declaring a subclass involves esoteric knowledge such as `Parent.call(this)` in order to pass in parameters to the parent class so that we can properly initialize the subclass, and setting the prototype of the subclass to an instance of the parent class's prototype. As you can readily find heaps of information about ((("prototypal inheritance", id="pi3)))prototypal inheritance around the web, we won't be delving into detailed minutia about prototypal inheritance.
[source,javascript]
----
function Banana() {
Fruit.call(this, 'banana', 105)
}
Banana.prototype = Object.create(Fruit.prototype)
Banana.prototype.slice = function () {
this.pieces = 12
}
----
Given the ephemeral knowledge one has to remember, and the fact that `Object.create` ((("Object.create")))was only made available in ES5, JavaScript developers have historically turned to libraries to resolve their prototype inheritance issues. One such example is `util.inherits` in Node.js, which ((("util.inherits")))is usually favored over `Object.create` for legacy support reasons.
[source,javascript]
----
const util = require('util')
function Banana() {
Fruit.call(this, 'banana', 105)
}
util.inherits(Banana, Fruit)
Banana.prototype.slice = function () {
this.pieces = 12
}
----
Consuming the `Banana` constructor is no different than how we used `Fruit`, except that the banana has a `name` and calories already assigned to it, and they come with an extra `slice` method we can use to promptly chop the banana instance into 12 pieces. The following piece of code shows the `Banana` in action as we take a bite.
[source,javascript]
----
const person = { satiety: 0 }
const banana = new Banana()
banana.slice()
banana.bite(person)
console.log(person.satiety)
// <- 8.75
console.log(banana.pieces)
// <- 11
console.log(banana.calories)
// <- 96.25
----
Classes consolidate prototypal inheritance, which up until recently had been highly contested in user-space by several libraries trying to make it easier to deal with prototypal inheritance in JavaScript.
The `Fruit` class is ripe for inheritance. In the following code snippet we create the `Banana` class as an extension of the `Fruit` class. Here, the syntax clearly signals our intent and we don't have to worry about thoroughly understanding prototypal inheritance in order to get to the results that we want. When we want to forward parameters to the underlying `Fruit` constructor, we can use `super`. The `super` keyword can also be used to call functions in the parent class, such as `super.chop`, and it's not just limited to the constructor for the parent class.
[source,javascript]
----
class Banana extends Fruit {
constructor() {
super('banana', 105)
}
slice() {
this.pieces = 12
}
}
----
Even though the `class` keyword is static we can still leverage JavaScript's flexible and functional properties when declaring classes. Any expression that returns a constructor function can be fed to `extends`. For example, we could have a constructor function factory and use that as the base class.
The following piece of code has a `createJuicyFruit` function where we forward the name and calories for a fruit to the `Fruit` class using a `super` call, and then all we have to do to create a `Plum` is extend the intermediary `JuicyFruit` class.
[source,javascript]
----
const createJuicyFruit = (...params) =>
class JuicyFruit extends Fruit {
constructor() {
this.juice = 0
super(...params)
}
squeeze() {
if (this.calories <= 0) {
return
}
this.calories -= 10
this.juice += 3
}
}
class Plum extends createJuicyFruit('plum', 30) {
}
----
Let's move ((("prototypal inheritance", startref="pi3)))onto `Symbol`. While not an iteration or flow control mechanism, learning about `Symbol` is crucial to shaping an understanding of iteration protocols, which are discussed at length in ((("classes", startref="class3")))((("classes", "extending", startref="class3ext")))the next chapter.
=== Symbols
Symbols ((("symbols", id="sym3")))are a new primitive type in ES6, and the seventh type in JavaScript. It is a unique value type, like strings and numbers. Unlike strings and numbers, symbols don't have a literal representation such as `'text'` for strings, or `1` for numbers. The purpose of symbols is primarily to ((("symbols", "defining protocols through")))implement protocols. For example, the iterable protocol ((("iterable protocol")))uses a symbol to define how objects are iterated, as we'll learn in <>.
There are three flavors of symbols, and each flavor is accessed in a different way. These are: local symbols, created with the `Symbol` built-in wrapper object and accessed by storing a reference or via reflection; global symbols, created using another API and shared across code realms; and "well-known" symbols, built into JavaScript and used to define internal language behavior.
We'll explore each of these, looking into possible use cases along the way. Let's begin with local symbols.
==== Local Symbols
Symbols ((("symbols", "local", id="sym3l")))((("local symbols", id="ls3")))can be created using the `Symbol` wrapper object. In the following piece of code, we create our `first` symbol.
[source,javascript]
----
const first = Symbol()
----
While you can use the `new` keyword with `Number` and `String`, the `new` operator throws a `TypeError` when we try it on `Symbol`. This avoids mistakes and confusing behavior like `new Number(3) !== Number(3)`. The following snippet shows the error being thrown.
[source,javascript]
----
const oops = new Symbol()
// <- TypeError, Symbol is not a constructor
----
For ((("symbols", "for debugging")))debugging purposes, you can create symbols using a description.
[source,javascript]
----
const mystery = Symbol('my symbol')
----
Like numbers or strings, symbols are immutable. Unlike other value types, however, symbols are unique. As shown in the next piece of code, descriptions don't affect that uniqueness. Symbols created using the same description are also unique and thus different from each other.
[source,javascript]
----
console.log(Number(3) === Number(3))
// <- true
console.log(Symbol() === Symbol())
// <- false
console.log(Symbol('my symbol') === Symbol('my symbol'))
// <- false
----
Symbols are of type `symbol`, new in ES6. The following snippet shows how `typeof` returns the new type string for symbols.
[source,javascript]
----
console.log(typeof Symbol())
// <- 'symbol'
console.log(typeof Symbol('my symbol'))
// <- 'symbol'
----
Symbols can be used as ((("symbols", "as property keys")))property keys on objects. Note how you can use a computed property name to avoid an extra statement just to add a `weapon` symbol key to the `character` object, as shown in the following example. Note also that, in order to access a symbol property, you'll need a reference to the symbol that was used to create said property.
[source,javascript]
----
const weapon = Symbol('weapon')
const character = {
name: 'Penguin',
[weapon]: 'umbrella'
}
console.log(character[weapon])
// <- 'umbrella'
----
Keep in mind that ((("symbols", "hidden properties of")))symbol keys are hidden from many of the traditional ways of pulling keys from an object. The next bit of code shows how `for..in`, `Object.keys`, and `Object.getOwnPropertyNames` fail to report on symbol properties.
[source,javascript]
----
for (let key in character) {
console.log(key)
// <- 'name'
}
console.log(Object.keys(character))
// <- ['name']
console.log(Object.getOwnPropertyNames(character))
// <- ['name']
----
This aspect of symbols means that code that was written before ES6 and without symbols in mind won't unexpectedly start stumbling upon symbols. In a similar fashion, as shown next, symbol properties are discarded when representing an object as JSON.
[source,javascript]
----
console.log(JSON.stringify(character))
// <- '{"name":"Penguin"}'
----
That being said, symbols are by no means a safe mechanism to conceal properties. Even though you won't stumble upon symbol properties when using pre-ES6 reflection, iteration or serialization methods, symbols are revealed by a dedicated method as shown in the next snippet of code. In other words, symbols are not nonenumerable, but hidden in plain sight. Using `Object.getOwnPropertySymbols` we can retrieve all symbols used as property keys on any given object.
[source,javascript]
----
console.log(Object.getOwnPropertySymbols(character))
// <- [Symbol(weapon)]
----
Now that we've established how symbols work, what can we use ((("symbols", "local", startref="sym3l")))((("local symbols", startref="ls3")))them for?
==== Practical Use Cases for Symbols
Symbols ((("symbols", "use cases for", id="sym3ucf")))((("symbols", "for DOM element mapping", id="sym3fdomem")))could be used by a library to map objects to DOM elements. For example, a library that needs to associate the API object for a calendar to the provided DOM element. Before ES6, there wasn't a clear way of mapping DOM elements to objects. You could add a property to a DOM element pointing to the API, but polluting DOM elements with custom properties is a bad practice. You have to be careful to use property keys that won't be used by other libraries, or worse, by the language itself in the future. That leaves you with using an array lookup table containing an entry for each DOM/API pair. That, however, might be slow in long-running applications where the array lookup table might grow in size, slowing down the lookup operation over time.
Symbols, on the other hand, don't have this problem. They can be used as properties that don't have a risk of clashing with future language features, as they're unique. The following code snippet shows how a symbol could be used to map DOM elements into calendar API objects.
[source,javascript]
----
const cache = Symbol('calendar')
function createCalendar(el) {
if (cache in el) { // does the symbol exist in the element?
return el[cache] // use the cache to avoid re-instantiation
}
const api = el[cache] = {
// the calendar API goes here
}
return api
}
----
There is an ((("symbols", "for DOM element mapping", startref="sym3fdomem")))ES6 built-in--the ++WeakMap++—that ((("WeakMap")))can be used to uniquely map objects to other objects without using arrays or placing foreign properties on the objects we want to be able to look up. In contrast with array lookup tables, `WeakMap` lookups are constant in time or O(1). We'll explore `WeakMap` in <>, alongside other ES6 collection built-ins.
===== Defining protocols through symbols
Earlier, ((("symbols", "defining protocols through", id="sym3dpt")))((("protocols", id="p3dts")))we posited that a use case for symbols is to define protocols. A protocol is a communication contract or convention that defines behavior. In less abstract terms, a library could use a symbol that could then be used by objects that adhere to a convention from the library.
Consider the following bit of code, where we use the special `toJSON` method to determine the object serialized by `JSON.stringify`. As you can see, stringifying the `character` object produces a serialized version of the object returned by `toJSON`.
[source,javascript]
----
const character = {
name: 'Thor',
toJSON: () => ({
key: 'value'
})
}
console.log(JSON.stringify(character))
// <- '"{"key":"value"}"'
----
In contrast, if `toJSON` was anything other than a function, the original `character` object would be serialized, including the `toJSON` property, as shown next. This sort of inconsistency ensues from relying on regular properties to define behavior.
[source,javascript]
----
const character = {
name: 'Thor',
toJSON: true
}
console.log(JSON.stringify(character))
// <- '"{"name":"Thor","toJSON":true}"'
----
The reason why it would be better to implement the `toJSON` modifier as a symbol is that that way it wouldn't interfere with other object keys. Given that symbols are unique, never serialized, and never exposed unless explicitly requested through `Object.getOwnPropertySymbols`, ((("Object.getOwnPropertySymbol")))they would represent a better choice when defining a contract between `JSON.stringify` and how objects want to be serialized. Consider the following piece of code with an alternative implementation of `toJSON` using a symbol to define serialization behavior for a `stringify` function.
[source,javascript]
----
const json = Symbol('alternative to toJSON')
const character = {
name: 'Thor',
[json]: () => ({
key: 'value'
})
}
stringify(character)
function stringify(target) {
if (json in target) {
return JSON.stringify(target[json]())
}
return JSON.stringify(target)
}
----
Using a symbol means we need to use a computed property name to define the `json` behavior directly on an object literal. It also means that the behavior won't clash with other user-defined properties or upcoming language features we couldn't foresee. Another difference is that the `json` symbol should be available to consumers of the `stringify` function, so that they can define their own behavior. We could easily add the following line of code to expose the `json` symbol directly through `stringify`, as shown next. That'd also tie the `stringify` function with the symbol that modifies its behavior.
[source,javascript]
----
stringify.as = json
----
By exposing the `stringify` function we'd be exposing the `stringify.as` symbol as well, allowing consumers to tweak behavior by minimally modifying objects, using the custom symbol.
When it comes to the merits of using a symbol to describe behavior, as opposed to an option passed to the `stringify` function, there are a few considerations to keep in mind. First, adding option parameters to a function changes its public API, whereas changing the internal implementation of the function to support another symbol wouldn't affect the public API. Using an `options` object with different properties for each option mitigates this effect, but it's not always convenient to require an `options` object in every function call.
A benefit of defining behavior via symbols is that you could augment and customize the behavior of objects without changing anything other than the value assigned to a symbol property and perhaps the internal implementation of the piece of code that leverages that behavior. The benefit of using symbols over properties is that you're not subject to name clashes when new language features are introduced.
Besides local symbols, there's also a global symbol registry, accessible from across code realms. Let's look into what ((("symbols", "use cases for", startref="sym3ucf")))((("symbols", "defining protocols through", startref="sym3dpt")))((("protocols", startref="p3dts")))that means.
==== Global Symbol Registry
A code ((("symbols", "global symbol registry", id="sym3gsr")))((("global symbol registry", id="gsr3")))realm is any JavaScript execution context, such as the page your application is running in, an `