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.
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.
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.
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.
You 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 around the web, we won’t be delving into detailed minutia about prototypal inheritance.
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
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 is usually favored over Object.create
for legacy support reasons.
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.
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.
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.
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 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 the next chapter.
Symbols 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 implement protocols. For example, the iterable protocol uses a symbol to define how objects are iterated, as we’ll learn in [iterator_protocol_and_iterable_protocol].
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.
Symbols can be created using the Symbol
wrapper object. In the following piece of code, we create our first
symbol.
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.
const oops = new Symbol()
// <- TypeError, Symbol is not a constructor
For debugging purposes, you can create symbols using a description.
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.
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.
console.log(typeof Symbol())
// <- 'symbol'
console.log(typeof Symbol('my symbol'))
// <- 'symbol'
Symbols can be used as 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.
const weapon = Symbol('weapon')
const character = {
name: 'Penguin',
[weapon]: 'umbrella'
}
console.log(character[weapon])
// <- 'umbrella'
Keep in mind that 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.
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.
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.
console.log(Object.getOwnPropertySymbols(character))
// <- [Symbol(weapon)]
Now that we’ve established how symbols work, what can we use them for?
Symbols 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.
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 ES6 built-in—the WeakMap—that 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 [leveraging-ecmascript-collections], alongside other ES6 collection built-ins.
Earlier, 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
.
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.
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
, 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.
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.
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 that means.
A code realm is any JavaScript execution context, such as the page your application is running in, an <iframe>
within that page, a script running through eval
, or a worker of any kind—such as web workers, service workers, or shared workers.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. Each of these execution contexts has its own global object. Global variables defined on the window
object of a page, for example, aren’t available to a ServiceWorker
. In contrast, the global symbol registry is shared across all code realms.
There are two methods that interact with the runtime-wide global symbol registry: Symbol.for
and Symbol.keyFor
. What do they do?
The Symbol.for(key)
method looks up key
in the runtime-wide symbol registry. If a symbol with the provided key
exists in the global registry, that symbol is returned. If no symbol with that key
is found in the registry, one is created and added to the registry under the provided key
. That’s to say, Symbol.for(key)
is idempotent: it looks for a symbol under a key
, creates one if it didn’t already exist, and then returns the symbol.
In the following code snippet, the first call to Symbol.for
creates a symbol identified as 'example'
, adds it to the registry, and returns it. The second call returns that same symbol because the key
is already in the registry—and associated to the symbol returned by the first call.
const example = Symbol.for('example')
console.log(example === Symbol.for('example'))
// <- true
The global symbol registry keeps track of symbols by their key
. Note that the key
will also be used as a description
when the symbols that go into the registry are created. Considering these symbols are global on a runtime-wide level, you might want to prefix symbol keys in the global registry with a value that identifies your library or component, mitigating potential name clashes.
Given a symbol symbol
, Symbol.keyFor(symbol)
returns the key
that was associated with symbol
when the symbol was added to the global registry. The next example shows how we can grab the key
for a symbol
using Symbol.keyFor
.
const example = Symbol.for('example')
console.log(Symbol.keyFor(example))
// <- 'example'
Note that if the symbol isn’t in the global runtime registry, then the method returns undefined
.
console.log(Symbol.keyFor(Symbol()))
// <- undefined
Also keep in mind that it’s not possible to match symbols in the global registry using local symbols, even when they share the same description. The reason for that is that local symbols aren’t part of the global registry, as shown in the following piece of code.
const example = Symbol.for('example')
console.log(Symbol.keyFor(Symbol('example')))
// <- undefined
Now that you’ve learned about the API for interacting with the global symbol registry, let’s take some considerations into account.
A runtime-wide registry means the symbols are accessible across code realms. The global registry returns a reference to the same object in any realm the code runs in. In the following example, we demonstrate how the Symbol.for
API returns the same symbol in a page and within an <iframe>
.
const d = document
const frame = d.body.appendChild(d.createElement('iframe'))
const framed = frame.contentWindow
const s1 = window.Symbol.for('example')
const s2 = framed.Symbol.for('example')
console.log(s1 === s2)
// <- true
There are trade-offs in using widely available symbols. On the one hand, they make it easy for libraries to expose their own symbols, but on the other hand they could also expose their symbols on their own API, using local symbols. The symbol registry is obviously useful when symbols need to be shared across any two code realms; for example, ServiceWorker
and a web page. The API is also convenient when you don’t want to bother storing references to the symbols. You could use the registry directly for that, since every call with a given key
is guaranteed to return the same symbol
. You’ll have to keep in mind, though, that these symbols are shared across the runtime and that might lead to unwanted consequences if you use generic symbol names like each
or contains
.
There’s one more kind of symbol: built-in well-known symbols.
So far we’ve covered symbols you can create using the Symbol
function and those you can create through Symbol.for
. The third and last kind of symbols we’re going to cover are the well-known symbols. These are built into the language instead of created by JavaScript developers, and they provide hooks into internal language behavior, allowing you to extend or customize aspects of the language that weren’t accessible prior to ES6.
A great example of how symbols can add extensibility to the language without breaking existing code is the Symbol.toPrimitive
well-known symbol. It can be assigned a function to determine how an object is cast into a primitive value. The function receives a hint
parameter that can be 'string'
, 'number'
, or 'default'
, indicating what type of primitive value is expected.
const morphling = {
[Symbol.toPrimitive](hint) {
if (hint === 'number') {
return Infinity
}
if (hint === 'string') {
return 'a lot'
}
return '[object Morphling]'
}
}
console.log(+morphling)
// <- Infinity
console.log(`That is ${ morphling }!`)
// <- 'That is a lot!'
console.log(morphling + ' is powerful')
// <- '[object Morphling] is powerful'
Another example of a well-known symbol is Symbol.match
. A regular expression that sets Symbol.match
to false
will be treated as a string literal when passed to .startsWith
, .endsWith
, or .includes
. These three functions are new string methods in ES6. First we have .startsWith
, which can be used to determine if the string starts with another string. Then there’s .endsWith
, which finds out whether the string ends in another one. Lastly, the .includes
method returns true
if a string contains another one. The next snippet of code shows how Symbol.match
can be used to compare a string with the string representation of a regular expression.
const text = '/an example string/'
const regex = /an example string/
regex[Symbol.match] = false
console.log(text.startsWith(regex))
// <- true
If the regular expression wasn’t modified through the symbol, it would’ve thrown because the .startsWith
method expects a string instead of a regular expression.
Well-known symbols are shared across realms. The following example shows how Symbol.iterator
is the same reference as that within the context of an <iframe>
window.
const frame = document.createElement('iframe')
document.body.appendChild(frame)
Symbol.iterator === frame.contentWindow.Symbol.iterator
// <- true
Note that even though well-known symbols are shared across code realms, they’re not in the global registry. The following bit of code shows that Symbol.iterator
produces undefined
when we ask for its key
in the registry. That means the symbol isn’t listed in the global registry.
console.log(Symbol.keyFor(Symbol.iterator))
// <- undefined
One of the most useful well-known symbols is Symbol.iterator
, used by a few different language constructs to iterate over a sequence, as defined by a function assigned to a property using that symbol on any object. In the next chapter we’ll go over Symbol.iterator
in detail, using it extensively along with the iterator and iterable protocols.
While we’ve already addressed syntax enhancements coming to object literals in [es6-essentials], there are a few new static methods available to the Object
built-in that we haven’t addressed yet. It’s time to take a look at what these methods bring to the table.
We’ve already looked at Object.getOwnPropertySymbols
, but let’s also take a look at Object.assign
, Object.is
, and Object.setPrototypeOf
.
The need to provide default values for a configuration object is not at all uncommon. Typically, libraries and well-designed component interfaces come with sensible defaults that cater to the most frequented use cases.
A Markdown library, for example, might convert Markdown into HTML by providing only an input
parameter. That’s its most common use case, simply parsing Markdown, and so the library doesn’t demand that the consumer provides any options. The library might, however, support many different options that could be used to tweak its parsing behavior. It could have an option to allow <script>
or <iframe>
tags, or an option to highlight keywords in code snippets using CSS.
Imagine, for example, that you want to provide a set of defaults like the one shown next.
const defaults = {
scripts: false,
iframes: false,
highlightSyntax: true
}
One possibility would be to use the defaults
object as the default value for the options
parameter, using destructuring. In this case, the users must provide values for every option whenever they decide to provide any options at all.
function md(input, options=defaults) {
}
The default values have to be merged with user-provided configuration, somehow. That’s where Object.assign
comes in, as shown in the following example. We start with an empty {}
object—which will be mutated and returned by Object.assign—we copy the default values over to it, and then copy the options on top. The resulting config
object will have all of the default values plus the user-provided configuration.
function md(input, options) {
const config = Object.assign({}, defaults, options)
}
The Object.assign
function mutates its first argument. Its signature is (target, …sources)
. Every source is applied onto the target object, source by source and property by property.
Consider the following scenario, where we don’t pass an empty object as the first argument of Object.assign
, instead just providing it with the defaults
and the options
. We would be changing the contents of the defaults
object, losing some of our default values—and obtaining some wrong ones—in the process of mutating the object. The first invocation would produce the same result as the previous example, but it would modify our defaults in the process, changing how subsequent calls to md
work.
function md(input, options) {
const config = Object.assign(defaults, options)
}
For this reason, it’s generally best to pass a brand new object on the first position, every time.
For any properties that had a default value where the user also provided a value, the user-provided value will prevail. Here’s how Object.assign
works. First, it takes the first argument passed to it; let’s call it target
. It then iterates over all keys of each of the other arguments; let’s call them sources
. For each source in sources
, all of its properties are iterated and assigned to target
. The end result is that rightmost sources—in our case, the options
object—overwrite any previously assigned values, as shown in the following bit of code.
const defaults = {
first: 'first',
second: 'second'
}
function applyDefaults(options) {
return Object.assign({}, defaults, options)
}
applyDefaults()
// <- { first: 'first', second: 'second' }
applyDefaults({ third: 3 })
// <- { first: 'first', second: 'second', third: 3 }
applyDefaults({ second: false })
// <- { first: 'first', second: false }
Before Object.assign
made its way into the language, there were numerous similar implementations of this technique in user-land JavaScript, with names like assign, or extend. Adding Object.assign
to the language consolidates these options into a single method.
Note that Object.assign
takes into consideration only own enumerable properties, including both string and symbol properties.
const defaults = {
[Symbol('currency')]: 'USD'
}
const options = {
price: '0.99'
}
Object.defineProperty(options, 'name', {
value: 'Espresso Shot',
enumerable: false
})
console.log(Object.assign({}, defaults, options))
// <- { [Symbol('currency')]: 'USD', price: '0.99' }
Note, however, that Object.assign
doesn’t cater to every need. While most user-land implementations have the ability to perform deep assignment, Object.assign
doesn’t offer a recursive treatment of objects. Object values are assigned as properties on target
directly, instead of being recursively assigned key by key.
In the following bit of code you might expect the f
property to be added to target.a
while keeping a.b
and a.d
intact, but the a.b
and a.d
properties are lost when using Object.assign
.
Object.assign({}, { a: { b: 'c', d: 'e' } }, { a: { f: 'g' } })
// <- { a: { f: 'g' } }
In the same vein, arrays don’t get any special treatment either. If you expected recursive behavior in Object.assign
the following snippet of code may also come as a surprise, where you may have expected the resulting object to have 'd'
in the third position of the array.
Object.assign({}, { a: ['b', 'c', 'd'] }, { a: ['e', 'f'] })
// <- { a: ['e', 'f'] }
At the time of this writing, there’s an ECMAScript stage 3 proposalYou can find the proposal draft at GitHub. to implement spread in objects, similar to how you can spread iterable objects onto an array in ES6. Spreading an object onto another is equivalent to using an Object.assign
function call.
The following piece of code shows a few cases where we’re spreading the properties of an object onto another one, and their Object.assign
counterpart. As you can see, using object spread is more succinct and should be preferred where possible.
const grocery = { ...details }
// Object.assign({}, details)
const grocery = { type: 'fruit', ...details }
// Object.assign({ type: 'fruit' }, details)
const grocery = { type: 'fruit', ...details, ...fruit }
// Object.assign({ type: 'fruit' }, details, fruit)
const grocery = { type: 'fruit', ...details, color: 'red' }
// Object.assign({ type: 'fruit' }, details, { color: 'red' })
As a counterpart to object spread, the proposal includes object rest properties, which is similar to the array rest pattern. We can use object rest whenever we’re destructuring an object.
The following example shows how we could leverage object rest to get an object containing only properties that we haven’t explicitly named in the parameter list. Note that the object rest property must be in the last position of destructuring, just like the array rest pattern.
const getUnknownProperties = ({ name, type, ...unknown }) =>
unknown
getUnknownProperties({
name: 'Carrot',
type: 'vegetable',
color: 'orange'
})
// <- { color: 'orange' }
We could take a similar approach when destructuring an object in a variable declaration statement. In the next example, every property that’s not explicitly destructured is placed in a meta
object.
const { name, type, ...meta } = {
name: 'Carrot',
type: 'vegetable',
color: 'orange'
}
// <- name = 'Carrot'
// <- type = 'vegetable'
// <- meta = { color: 'orange' }
We dive deeper into object rest and spread in [practical-considerations].
The Object.is
method is a slightly different version of the strict equality comparison operator, ===
. For the most part, Object.is(a, b)
is equal to a === b
. There are two differences: the case of NaN
and the case of -0
and +0
. This algorithm is referred to as SameValue
in the ECMAScript specification.
When NaN
is compared to NaN
, the strict equality comparison operator returns false
because NaN
is not equal to itself. The Object.is
method, however, returns true
in this special case.
NaN === NaN
// <- false
Object.is(NaN, NaN)
// <- true
Similarly, when -0
is compared to +0
, the ===
operator produces true
while Object.is
returns false
.
-0 === +0
// <- true
Object.is(-0, +0)
// <- false
These differences may not seem like much, but dealing with NaN
has always been cumbersome because of its special quirks, such as typeof NaN
being 'number'
and it not being equal to itself.
The Object.setPrototypeOf
method does exactly what its name conveys: it sets the prototype of an object to a reference to another object. It’s considered the proper way of setting the prototype, as opposed to using proto
, which is a legacy feature.
Before ES6, we were introduced to Object.create
in ES5. Using that method, we could create an object based on any prototype passed into Object.create
, as shown next.
const baseCat = { type: 'cat', legs: 4 }
const cat = Object.create(baseCat)
cat.name = 'Milanesita'
The Object.create
method is, however, limited to newly created objects. In contrast, we could use Object.setPrototypeOf
to change the prototype of an object that already exists, as shown in the following code snippet.
const baseCat = { type: 'cat', legs: 4 }
const cat = Object.setPrototypeOf(
{ name: 'Milanesita' },
baseCat
)
Note however that there are serious performance implications when using Object.setPrototypeOf
as opposed to Object.create
, and some careful consideration is in order before you decide to go ahead and sprinkle Object.setPrototypeOf
all over a codebase.
Using Object.setPrototypeOf
to change the prototype of an object is an expensive operation. Here is what the Mozilla Developer Network documentation has to say about the matter:
Changing the prototype of an object is, by the nature of how modern JavaScript engines optimize property accesses, a very slow operation, in every browser and JavaScript engine. The effects on performance of altering inheritance are subtle and far-flung, and are not limited to simply the time spent in a
Object.setPrototypeOf(…)
statement, but may extend to any code that has access to any object whose prototype has been altered. If you care about performance you should avoid setting the prototype of an object. Instead, create a new object with the desired prototype usingObject.create()
.
Decorators are, as most things programming, definitely not a new concept. The pattern is fairly commonplace in modern programming languages: you have attributes in C#, they’re called annotations in Java, there are decorators in Python, and the list goes on. There’s a JavaScript decorators proposal in the works and you can find the draft online at GitHub. It is currently sitting at stage 2 of the TC39 process.
The syntax for JavaScript decorators is fairly similar to that of Python decorators. JavaScript decorators may be applied to classes and any statically defined properties, such as those found on an object literal declaration or in a class
declaration—even if they are get
accessors, set
accessors, or static
properties.
The proposal defines a decorator as an @
followed by a sequence of dotted identifiers[1] notation is disallowed due to the difficulty it would present when disambiguating grammar at the compiler level.] and an optional argument list. Here are a few examples:
-
@decorators.frozen
is a valid decorator -
@decorators.frozen(true)
is a valid decorator -
@decorators().frozen()
is a syntax error -
@decorators['frozen']
is a syntax error
Zero or more decorators can be attached to class
declarations and class members.
@inanimate
class Car {}
@expensive
@speed('fast')
class Lamborghini extends Car {}
class View {
@throttle(200) // reconcile once every 200ms at most
reconcile() {}
}
Decorators are implemented by way of functions. Member decorator functions take a member descriptor and return a member descriptor. Member descriptors are similar to property descriptors, but with a different shape. The following bit of code has the member descriptor interface, as defined by the decorators proposal. An optional finisher
function receives the class constructor, allowing us to perform operations related to the class whose property is being decorated.
interface MemberDescriptor { kind: "Property" key: string, isStatic: boolean, descriptor: PropertyDescriptor, extras?: MemberDescriptor[] finisher?: (constructor): void; }
In the following example we define a readonly
member decorator function that makes decorated members nonwritable. Taking advantage of the object rest parameter and object spread, we modify the property descriptor to be non-writable while keeping the rest of the member descriptor unchanged.
function readonly({ descriptor, ...rest }) {
return {
...rest,
descriptor: {
...descriptor,
writable: false
}
}
}
Class decorator functions take a ctor
, which is the class constructor being decorated; a heritage
parameter, containing the parent class when the decorated class extends another class; and a members
array, with a list of member descriptors for the class being decorated.
We could implement a class-wide readonlyMembers
decorator by reusing the readonly
member decorator on each member descriptor for a decorated class, as shown next.
function readonlyMembers(ctor, heritage, members) {
return members.map(member => readonly(member))
}
With all the fluff around immutability you may be tempted to return a new property descriptor from your decorators, without modifying the original descriptor. While well-intentioned, this may have an undesired effect, as it is possible to decorate the same class
or class member several times.
If any decorators in a piece of code returned an entirely new descriptor
without taking into consideration the descriptor
parameter they receive, they’d effectively lose all the decoration that took place before the different descriptor was returned.
We should be careful to write decorators that take into account the supplied descriptor
. Always create one that’s based on the original descriptor
that’s provided as a parameter.
A long time ago, I was first getting acquainted with C# by way of an Ultima Online[2] server emulator written in open source C# code—RunUO. RunUO was one of the most beautiful codebases I’ve ever worked with, and it was written in C# to boot.
They distributed the server software as an executable and a series of .cs
files. The runuo
executable would compile those .cs
scripts at runtime and dynamically mix them into the application. The result was that you didn’t need the Visual Studio IDE (nor msbuild
), or anything other than just enough programming knowledge to edit one of the "scripts" in those .cs
files. All of the above made RunUO the perfect learning environment for the new developer.
RunUO relied heavily on reflection. RunUO’s developers made significant efforts to make it customizable by players who were not necessarily invested in programming, but were nevertheless interested in changing a few details of the game, such as how much damage a dragon’s fire breath inflicts or how often it shot fireballs. Great developer experience was a big part of their philosophy, and you could create a new kind of Dragon
just by copying one of the monster files, changing it to inherit from the Dragon
class, and overriding a few properties to change its color hue, its damage output, and so on.
Just as they made it easy to create new monsters—or "non-player characters" (NPC in gaming slang)--they also relied on reflection to provide functionality to in-game administrators. Administrators could run an in-game command and click on an item or a monster to visualize or change properties without ever leaving the game.
Not every property in a class is meant to be accessible in-game, though. Some properties are only meant for internal use, or not meant to be modified at runtime. RunUO had a CommandPropertyAttribute
decorator,The RunUO Git repository has the definition of CommandPropertyAttribute
for RunUO. which defined that the property could be modified in-game and let you also specify the access level required to read and write that property. This decorator was used extensively throughout the RunUO codebase.Its use is widespread throughout the codebase, marking over 200 properties in the RunUO core alone.
The PlayerMobile
class, which governed how a player’s character works, is a great place to look at these attributes. PlayerMobile
has several properties that are accessible in-gameYou can find quite a few usage examples of the CommandProperty
attribute in the PlayerMobile.cs
class. to administrators and moderators. Here are a couple of getters and setters, but only the first one has the CommandProperty
attribute—making that property accessible to Game Masters in-game.
[CommandProperty(AccessLevel.GameMaster)]
public int Profession
{
get{ return m_Profession }
set{ m_Profession = value }
}
public int StepsTaken
{
get{ return m_StepsTaken }
set{ m_StepsTaken = value }
}
One interesting difference between C# attributes and JavaScript decorators is that reflection in C# allows us to pull all custom attributes from an object using MemberInfo#getCustomAttributes
. RunUO leverages that method to pull up information about each property that should be accessible in-game when displaying the dialog that lets an administrator view or modify an in-game object’s properties.
In JavaScript, there’s no such thing—not in the existing proposal draft, at least—to get the custom attributes on a property. That said, JavaScript is a highly dynamic language, and creating this sort of "labels" wouldn’t be much of a hassle. Decorating a Dog
with a "command property" wouldn’t be all that different from RunUO and C#.
class Dog {
@commandProperty('game-master')
name;
}
The commandProperty
function would need to be a little more sophisticated than its C# counterpart. Given that there is no reflection around JavaScript decorators[3], we could use a runtime-wide symbol to keep around an array of command properties for any given class.
function commandProperty(writeLevel, readLevel = writeLevel) {
return ({ key, ...rest }) => ({
key,
...rest,
finisher(ctor) {
const symbol = Symbol.for('commandProperties')
const commandPropertyDescriptor = {
key,
readLevel,
writeLevel
}
if (!ctor[symbol]) {
ctor[symbol] = []
}
ctor[symbol].push(commandPropertyDescriptor)
}
})
}
A Dog
class could have as many command properties as we deemed necessary, and each would be listed behind a symbol property. To find the command properties for any given class, all we’d have to do is use the following function, which retrieves a list of command properties from the symbol property, and offers a default value of []. We always return a copy of the original list to prevent consumers from accidentally making changes to it.
function getCommandProperties(ctor) {
const symbol = Symbol.for('commandProperties')
const properties = ctor[symbol] || []
return [...properties]
}
getCommandProperties(Dog)
// <- [{ key: 'name', readLevel: 'game-master',
// writeLevel: 'game-master' }]
We could then iterate over known safe command properties and render a way of modifying those during runtime, through a simple UI. Instead of maintaining long lists of properties that can be modified, relying on some sort of heuristics bound to break from time to time, or using some sort of restrictive naming convention, decorators are the cleanliest way to implement a protocol where we mark properties as special for some particular use case.
In the following chapter we’ll look at more features coming in ES6 and how they can be used to iterate over any JavaScript objects, as well as how to master flow control using promises and generators.
[
2. Ultima Online is a decades-old fantasy role playing game based on the Ultima universe.
3. 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.