“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.
[]
notation is disallowed due to the difficulty it would present when disambiguating grammar at the compiler level.CommandPropertyAttribute
for RunUO.CommandProperty
attribute in the PlayerMobile.cs
class.