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.
functionFruit(name,calories){this.name=namethis.calories=caloriesthis.pieces=1}Fruit.prototype.chop=function(){this.pieces++}Fruit.prototype.bite=function(person){if(this.pieces<1){return}constcalories=this.calories/this.piecesperson.satiety+=caloriesthis.calories-=caloriesthis.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.
constperson={satiety:0}constapple=newFruit('apple',140)apple.chop()apple.chop()apple.chop()apple.bite(person)apple.bite(person)apple.bite(person)console.log(person.satiety)// <- 105console.log(apple.pieces)// <- 1console.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.
classFruit{constructor(name,calories){this.name=namethis.calories=caloriesthis.pieces=1}chop(){this.pieces++}bite(person){if(this.pieces<1){return}constcalories=this.calories/this.piecesperson.satiety+=caloriesthis.calories-=caloriesthis.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.
newPerson()// <- ReferenceError: Person is not definedclassPerson{}
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.
constPerson=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().
constcreatePersonClass=name=>classextendsPerson{constructor(){super(name)}}constJakePerson=createPersonClass('Jake')constjake=newJakePerson()
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.
classFruit{}functionFruit(){}
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.
classLog{constructor(...args){console.log(args)}}newLog('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.
classCounter{constructor(start){this.count=start}getnext(){returnthis.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.
constcounter=newCounter(2)console.log(counter.next)// <- 2console.log(counter.next)// <- 3console.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.
classLocalStorage{constructor(key){this.key=key}getdata(){returnJSON.parse(localStorage.getItem(this.key))}setdata(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.
constls=newLocalStorage('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.
classPerson{constructor(){this.satiety=0}eat(fruit){while(fruit.pieces>0){fruit.bite(this)}}}constplum=newFruit('plum',40)constperson=newPerson()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.
functionPerson(){this.hunger=100}Person.prototype.eat=function(){this.hunger--}Person.isPerson=function(person){returnpersoninstanceofPerson}
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.
classMathHelper{staticsum(...numbers){returnnumbers.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.
functionBanana(){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.
constutil=require('util')functionBanana(){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.
constperson={satiety:0}constbanana=newBanana()banana.slice()banana.bite(person)console.log(person.satiety)// <- 8.75console.log(banana.pieces)// <- 11console.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.
classBananaextendsFruit{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.
constcreateJuicyFruit=(...params)=>classJuicyFruitextendsFruit{constructor(){this.juice=0super(...params)}squeeze(){if(this.calories<=0){return}this.calories-=10this.juice+=3}}classPlumextendscreateJuicyFruit('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 later in the 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 Section 4.2: 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.
constfirst=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.
constoops=newSymbol()// <- TypeError, Symbol is not a constructor
For debugging purposes, you can create symbols using a description.
constmystery=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))// <- trueconsole.log(Symbol()===Symbol())// <- falseconsole.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(typeofSymbol())// <- 'symbol'console.log(typeofSymbol('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.
constweapon=Symbol('weapon')constcharacter={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(letkeyincharacter){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 reflection 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.
constcache=Symbol('calendar')functioncreateCalendar(el){if(cacheinel){// does the symbol exist in the element?returnel[cache]// use the cache to avoid re-instantiation}constapi=el[cache]={// the calendar API goes here}returnapi}
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 Chapter 5, 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.
constcharacter={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.
constcharacter={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.
constjson=Symbol('alternative to toJSON')constcharacter={name:'Thor',[json]:()=>({key:'value'})}stringify(character)functionstringify(target){if(jsonintarget){returnJSON.stringify(target[json]())}returnJSON.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.1 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.
constexample=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.
constexample=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.
constexample=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>.
constd=documentconstframe=d.body.appendChild(d.createElement('iframe'))constframed=frame.contentWindowconsts1=window.Symbol.for('example')consts2=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.
constmorphling={[Symbol.toPrimitive](hint){if(hint==='number'){returnInfinity}if(hint==='string'){return'a lot'}return'[object Morphling]'}}console.log(+morphling)// <- Infinityconsole.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.
consttext='/an example string/'constregex=/an example string/regex[Symbol.match]=falseconsole.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.
constframe=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 Chapter 2, 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.
constdefaults={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.
functionmd(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.
functionmd(input,options){constconfig=Object.assign({},defaults,options)}
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.
constdefaults={first:'first',second:'second'}functionapplyDefaults(options){returnObject.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.
constdefaults={[Symbol('currency')]:'USD'}constoptions={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 b.c and b.d intact, but the b.c and b.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 proposal2 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.
constgrocery={...details}// Object.assign({}, details)constgrocery={type:'fruit',...details}// Object.assign({ type: 'fruit' }, details)constgrocery={type:'fruit',...details,...fruit}// Object.assign({ type: 'fruit' }, details, fruit)constgrocery={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.
constgetUnknownProperties=({name,type,...unknown})=>unknowngetUnknownProperties({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 Chapter 9.
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// <- falseObject.is(NaN,NaN)// <- true
Similarly, when -0 is compared to +0, the === operator produces true while Object.is returns false.
-0===+0// <- trueObject.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.
constbaseCat={type:'cat',legs:4}constcat=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.
constbaseCat={type:'cat',legs:4}constcat=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.
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 proposal3 in the works. 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 identifiers4 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.
@inanimateclassCar{}@expensive@speed('fast')classLamborghiniextendsCar{}classView{@throttle(200)// reconcile once every 200ms at mostreconcile(){}}
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.
functionreadonly({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.
functionreadonlyMembers(ctor,heritage,members){returnmembers.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 Online5 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,6 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.7
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-game8 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)]publicintProfession{get{returnm_Profession}set{m_Profession=value}}publicintStepsTaken{get{returnm_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#.
classDog{@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 decorators9, we could use a runtime-wide symbol to keep around an array of command properties for any given class.
functioncommandProperty(writeLevel,readLevel=writeLevel){return({key,...rest})=>({key,...rest,finisher(ctor){constsymbol=Symbol.for('commandProperties')constcommandPropertyDescriptor={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.
functiongetCommandProperties(ctor){constsymbol=Symbol.for('commandProperties')constproperties=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 Workers are a way of executing background tasks in browsers. The initiator can communicate with their workers, which run in a different execution context, via messaging.
2 You can find the proposal draft at GitHub.
3 You can find the proposal draft online at GitHub.
4 Accessing properties via [] notation is disallowed due to the difficulty it would present when disambiguating grammar at the compiler level.
5 Ultima Online is a decades-old fantasy role playing game based on the Ultima universe.
6 The RunUO Git repository has the definition of CommandPropertyAttribute for RunUO.
7 Its use is widespread throughout the codebase, marking over 200 properties in the RunUO core alone.
8 You can find quite a few usage examples of the CommandProperty attribute in the PlayerMobile.cs class.
9 Reflection around JavaScript decorators is not being considered for JavaScript at this time, as it’d involve engines keeping more metadata in memory. We can, however, use symbols and lists to get around the need for native reflection.