Skip to content

Latest commit

 

History

History
1052 lines (826 loc) · 58.9 KB

ch03.asciidoc

File metadata and controls

1052 lines (826 loc) · 58.9 KB

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.

Extending JavaScript Classes

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

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.

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?

Practical Use Cases for Symbols

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.

Defining protocols through symbols

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.

Global Symbol Registry

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?

Getting symbols with Symbol.for(key)

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.

Using Symbol.keyFor(symbol) to retrieve symbol keys

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.

Best practices and considerations

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.

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.

Shared across realms but not in the registry

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.

Object Built-in Improvements

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.

Extending Objects with Object.assign

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)
}
Understanding the Target of Object.assign

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].

Comparing Objects with Object.is

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.

Object.setPrototypeOf

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.

Performance Issues

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 using Object.create().

— Mozilla Developer Network

Decorators

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.

A Primer on JavaScript Decorators

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))
}

Stacking Decorators and a Warning About Immutability

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.

Use Case By Example: Attributes in C#

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.

Modifying properties for a RunUO item in-game from the Ultima Online client.
Figure 1. Modifying properties for a RunUO item in-game from the Ultima Online client

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.

Marking Properties in JavaScript

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.


1. Accessing properties via [
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.