[[practical-considerations]]
== Practical Considerations
JavaScript is an ever-evolving language. Its development rhythm has had different paces throughout the years, entering a high-velocity phase with the introduction of ES5. Thus far, this book has taught you about dozens of language features and syntax changes introduced in ES6, and a few that came out afterwards, in ES2016 and ES2017.
Reconciling all of these new features with our existing ES5 knowledge may seem like a daunting task: what features should we take advantage of, and how? This chapter aims to rationalize the choices we have to make when considering whether to use specific ES6 features.
We'll take a look at a few different features, the use cases where they shine, and the situations where we might be better off using features that were already available in the language. Let's go case by case.
=== Variable Declarations
When ((("variable declarations", id="vd9")))developing software, most of our time is spent reading code, instead of writing it. ES6 ((("let statements", id="ls9")))((("const statements", id="cs9")))offers `let` and `const` as new flavors of variable declaration, and part of the value in these statements is that they can signal how a variable is used. When reading a piece of code, others can take cues from these signals in order to better understand what we did. Cues like these are crucial to reducing the amount of time someone spends interpreting what a piece of code does, and as such we should try and leverage them whenever possible.
A `let` statement indicates that a variable can't be used before its declaration, due to the Temporal Dead Zone rule. This isn't a convention, it is a fact: if we tried accessing the variable before its declaration statement was reached, the program would fail. These statements are block-scoped and not function-scoped; this means we need to read less code in order to fully grasp how a `let` variable is used.
The `const` statement is block-scoped as well, and it follows TDZ semantics too. The upside is that a `const` binding can only be assigned during declaration.
Note that this means that the variable binding can't change, but it doesn't mean that the value itself is immutable or constant in any way. A `const` binding that references an object can't later reference a different value, but the underlying object can indeed mutate.
In addition to the signals offered by `let`, the `const` keyword indicates that a variable binding can't be reassigned. This is a strong signal. You know what the value is going to be; you know that the binding can't be accessed outside of its immediately containing block, due to block scoping; and you know that the binding is never accessed before declaration, because of TDZ semantics.
You know all of this just by reading the `const` declaration statement and without scanning for other references to that variable.
Constraints such as those offered by `let` and `const` are a powerful way of making code easier to understand. Try to accrue as many of these constraints as possible in the code you write. The more declarative constraints that limit what a piece of code could mean, the easier and faster it is for humans to read, parse, and understand a piece of code in the future.
Granted, there are more rules to a `const` declaration than to a `var` declaration: block-scoped, TDZ, assign at declaration, no reassignment, whereas `var` statements only signal function scoping. Rule-counting, however, doesn't offer a lot of insight. It is better to weigh these rules in terms of complexity: does the rule add or subtract complexity? In the case of `const`, block scoping means a narrower scope than function scoping, TDZ means that we don't need to scan the scope backward from the declaration in order to spot usage before declaration, and assignment rules mean that the binding will always preserve the same reference.
The more constrained statements are, the simpler a piece of code becomes. As we add constraints to what a statement might mean, code becomes less unpredictable. This is one of the reasons why statically typed programs are, generally speaking, a bit easier to read than their dynamically typed counterparts. Static typing places a big constraint on the program writer, but it also places a big constraint on how the program can be interpreted, making its code easier to understand.
With these arguments in mind, it is recommended that you use `const` where possible, as it's the statement that gives us the fewest possibilities to think about.
[source,javascript]
----
if (condition) {
// can't access `isReady` before declaration is reached
const isReady = true
// `isReady` binding can't be reassigned
}
// can't access `isReady` outside of its containing block scope
----
When `const` isn't an option, because the variable needs to be reassigned later, we may resort to a `let` statement. Using `let` carries all the benefits of `const`, except that the variable can be reassigned. This may be necessary in order to increment a counter, flip a Boolean flag, or defer initialization.
Consider the following example, where we take a number of megabytes and return a string such as `1.2 GB`. We're using `let`, as the values need to change if a condition is met.
[source,javascript]
----
function prettySize(input) {
let value = input
let unit = 'MB'
if (value >= 1024) {
value /= 1024
unit = 'GB'
}
if (value >= 1024) {
value /= 1024
unit = 'TB'
}
return `${ value.toFixed(1) } ${ unit }`
}
----
Adding support for petabytes would involve a new `if` branch before the `return` statement.
[source,javascript]
----
if (value >= 1024) {
value /= 1024
unit = 'PB'
}
----
If we were looking to make `prettySize` easier to extend with new units, we could consider implementing a `toLargestUnit` function that computes the `unit` and `value` for any given `input` and its current unit. We could then consume `toLargestUnit` in `prettySize` to return the formatted string.
The following code snippet implements such a function. It relies on a list of supported `units` instead of using a new branch for each unit. When the input `value` is at least `1024` and there are larger units, we divide the input by `1024` and move to the next unit. Then we call `toLargestUnit` with the updated values, which will continue recursively reducing the `value` until it's small enough or we reach the largest unit.
[source,javascript]
----
function toLargestUnit(value, unit = 'MB') {
const units = ['MB', 'GB', 'TB']
const i = units.indexOf(unit)
const nextUnit = units[i + 1]
if (value >= 1024 && nextUnit) {
return toLargestUnit(value / 1024, nextUnit)
}
return { value, unit }
}
----
Introducing petabyte support used to involve a new `if` branch and repeating logic, but now it's only a matter of adding the `'PB'` string at the end of the `units` array.
The `prettySize` function becomes concerned only with how to display the string, as it can offload its calculations to the `toLargestUnit` function. This separation of concerns is also instrumental in producing more readable code.
[source,javascript]
----
function prettySize(input) {
const { value, unit } = toLargestUnit(input)
return `${ value.toFixed(1) } ${ unit }`
}
----
Whenever a piece of code has variables that need to be reassigned, we should spend a few minutes thinking about whether there's a better pattern that could resolve the same problem without reassignment. This is not always possible, but it can be accomplished most of the time.
Once you've arrived at a different solution, compare it to what you used to have. Make sure that code readability has actually improved and that the implementation is still correct. Unit tests can be instrumental in this regard, as they'll ensure you don't run into the same shortcomings twice. If the refactored piece of code seems worse in terms of readability or extensibility, carefully consider going back to the previous solution.
Consider the following contrived example, where we use array concatenation to generate the `result` array. Here, too, we could change from `let` to `const` by making a simple adjustment.
[source,javascript]
----
function makeCollection(size) {
let result = []
if (size > 0) {
result = result.concat([1, 2])
}
if (size > 1) {
result = result.concat([3, 4])
}
if (size > 2) {
result = result.concat([5, 6])
}
return result
}
makeCollection(0) // <- []
makeCollection(1) // <- [1, 2]
makeCollection(2) // <- [1, 2, 3, 4]
makeCollection(3) // <- [1, 2, 3, 4, 5, 6]
----
We can replace the reassignment operations with `Array#push`, which accepts multiple values. If we had a dynamic list, we could use the spread operator to push as many `...items` as necessary.
[source,javascript]
----
function makeCollection(size) {
const result = []
if (size > 0) {
result.push(1, 2)
}
if (size > 1) {
result.push(3, 4)
}
if (size > 2) {
result.push(5, 6)
}
return result
}
makeCollection(0) // <- []
makeCollection(1) // <- [1, 2]
makeCollection(2) // <- [1, 2, 3, 4]
makeCollection(3) // <- [1, 2, 3, 4, 5, 6]
----
When you do need to use `Array#concat`, you might prefer to use `[...result, 1, 2]` instead, to make the code shorter.
The last case we'll cover is one of refactoring. Sometimes, we write code like the next snippet, usually in the context of a larger function.
[source,javascript]
----
let completionText = 'in progress'
if (completionPercent >= 85) {
completionText = 'almost done'
} else if (completionPercent >= 70) {
completionText = 'reticulating splines'
}
----
In these cases, it makes sense to extract the logic into a pure function. This way we avoid the initialization complexity near the top of the larger function, while clustering all the logic about computing the completion text in one place.
The following piece of code shows how we could extract the completion text logic into its own function. We can then move `getCompletionText` out of the way, making the code more linear in terms of ((("variable declarations", startref="vd9")))((("let statements", startref="ls9")))((("const statements", startref="cs9")))readability.
[source,javascript]
----
const completionText = getCompletionText(completionPercent)
// …
function getCompletionText(progress) {
if (progress >= 85) {
return 'almost done'
}
if (progress >= 70) {
return 'reticulating splines'
}
return 'in progress'
}
----
=== Template Literals
For ((("template literals", id="tl9")))the longest time, JavaScript users have resorted to utility libraries to format strings, as that was never a part of the language until now. Creating a multiline string was also a hassle, as was escaping single or double quotes--depending on which quote style you were using. Template literals are different, and they fix all of these inconveniences.
With a template literal, you can use ((("expression interpolation")))expression interpolation, which enables you to inline variables, function calls, or any other arbitrary JavaScript expressions in a string without relying on concatenation.
[source,javascript]
----
'Hello, ' + name + '!' // before
`Hello, ${ name }!` // after
----
Multiline strings ((("multiline strings")))((("strings", "multiline")))such as the one shown in the following snippet involve one or more of array concatenation, string concatenation, or explicit `\n` line feeds. The code is a typical example for writing an HTML string in the pre-ES6 era.
[source,javascript]
----
'
' `
'
' `
'Hello' `
'' + name + '' `
'!' `
'
' `
'
'
----
Using template literals, we can avoid all of the extra quotes and concatenation, focusing on the content. The interpolation certainly helps in these kinds of templates, making multiline strings one of the most useful aspects of template literals.
[source,javascript]
----
``
----
When it comes to quotes, `'` and `"` are more likely to be necessary when writing a string than +`+ is. For the average English phrase, you're less likely to require backticks than single or double quotes. This means that backticks lead to less escaping.footnote:[Typography enthusiasts will be quick to point out that straight quotes are typographically incorrect, meaning we should be using “ ” ‘ ’, which don't lead to escaping. The fact remains that in practice we use straight quotes in code simply because they're easier to type. Meanwhile, typographic beautification is usually offloaded to utility libraries or a compilation step such as within a Markdown compiler.]
[source,javascript]
----
'Alfred\'s cat suit is "slick".'
"Alfred's cat suit is \"slick\"."
`Alfred's cat suit is "slick".`
----
As we discovered in <>, there are also other features such as ((("tagged templates")))tagged templates, which make it easy to sanitize or otherwise manipulate interpolated expressions. While useful, tagged templates are not as pervasively beneficial as multiline support, expression interpolation, or reduced escaping.
The combination of all of these features warrants considering template literals as the default string flavor over ((("template literals", "quotes and backticks", id="tl9qab")))single- or double-quoted strings. There are a few concerns usually raised when template literals are proposed as the default style. We'll go over each concern and address each individually. You can then decide for yourself.
Before we begin, let's set a starting point everyone agrees on: using template literals when an expression has to be interpolated in a string is better than using ((("quoted string concatenation")))quoted string concatenation.
Performance is often one of the cited concerns: is using template literals everywhere going to harm my application's performance? When using a compiler like ((("Babel")))Babel, template literals are transformed into quoted strings and interpolated expressions are concatenated amid those strings.
Consider the following example using template literals.
[source,javascript]
----
const suitKind = `cat`
console.log(`Alfred's ${ suitKind } suit is "slick".`)
// <- Alfred's cat suit is "slick".
----
A compiler such as Babel would transform our example into code similar to this, relying on quoted strings.
[source,javascript]
----
const suitKind = 'cat'
console.log('Alfred\'s ' + suitKind + ' suit is "slick".')
// <- Alfred's cat suit is "slick".
----
We've already settled that interpolated expressions are better than quoted string concatenation, in terms of readability, and the compiler turns those into quoted string concatenation, maximizing browser support.
When it comes to the `suitKind` variable, a template literal with no interpolation, no newlines, and no tags, the compiler simply turns it into a plain quoted string.
Once we stop compiling template literals down to quoted strings, we can expect optimizing compilers to be able to interpret them as such with negligible slowdown.
Another often-cited concern is syntax: as of this writing, we can't use backtick strings in JSON, object keys, `import` declarations, or strict mode directives.
The first statement in the following snippet of code demonstrates that a serialized JSON object couldn't represent strings using backticks. As shown on the second line, we can certainly declare an object using template literals and then serialize that object as JSON. By the time `JSON.stringify` is invoked, the template literal has evaluated to a quoted string.
[source,javascript]
----
JSON.parse('{ "payload": `message` }')
// <- SyntaxError
JSON.stringify({ payload: `message` })
// <- '{"payload":"message"}'
----
When it comes to object keys, we're out of luck. Attempting to use a template literal would result in a syntax error.
[source,javascript]
----
const alfred = { `suit kind`: `cat` }
----
Object property keys ((("object property keys")))accept value types, which are then cast into plain strings, but template literals aren't value types, and thus it's not possible to use them as property keys.
As you might recall from <>, ES6 introduces computed property names, as seen in the following code snippet. In a ((("computed property keys")))computed property key we can use any expression we want to produce the desired property key, including template literals.
[source,javascript]
----
const alfred = { [`suit kind`]: `cat` }
----
The preceding is far from ideal due to its verbosity, though, and in these cases it's best to use regular quoted strings.
As always, the rule is to never take rules such as "template literals are the best option" too literally, and be open to use your best judgment as necessary and break the rules a little bit, if they don't quite fit your use cases, conventions, or view of how an application is best structured. Rules are often presented as such, but what may be a rule to someone need not be a rule to everyone. This is the main reason why modern linters make every rule optional: the rules we use should be enforced, but not every rule may fit every project.
Perhaps someday we might get a flavor of computed property keys that doesn't rely on square brackets for template literals, saving us a couple of characters when we need to interpolate a string. For the foreseeable future, the following code snippet will result in a syntax error.
[source,javascript]
----
const brand = `Porsche`
const car = {
`wheels`: 4,
`has fuel`: true,
`is ${ brand }`: `you wish`
}
----
Attempts to import a module using template literals will also result in a syntax error. This is one of those cases where we might expect to be able to use template literals, if we were to adopt them extensively throughout our codebase, but can't.
[source,javascript]
----
import { SayHello } from `./World`
----
Strict mode directives have to be single- or double-quoted strings. As of this writing, there's no plan to allow template literals for `'use strict'` directives. The following piece of code does not result in a syntax error, but it also does not enable ((("strict mode")))strict mode. This is the biggest caveat when heavily using template literals.
[source,javascript]
----
'use strict' // enables strict mode
"use strict" // enables strict mode
`use strict` // nothing happens
----
Lastly, it could be argued that turning an existing codebase from single-quoted strings to template literals would be error-prone and a waste of time that could be otherwise used to develop features or fix bugs.
Fortunately, we ((("ESLint")))have `eslint` at our disposal, as discussed in <>. To switch our codebase to backticks by default, we can set up an _.eslintrc.json_ configuration similar to the one in the following piece of code. Note how we turn the `quotes` rule into an error unless the code uses backticks.
[source,json]
----
{
"env": {
"es6": true
},
"extends": "eslint:recommended",
"rules": {
"quotes": ["error", "backtick"]
}
}
----
With that in place, we can add a `lint` script to our _package.json_, like the one in the next snippet. The `--fix` flag ensures that any style errors found by the linter, such as using single quotes over backticks, are autocorrected.
[source,json]
----
{
"scripts": {
"lint": "eslint --fix ."
}
}
----
Once we run the following command, we're ready to start experimenting with a codebase that uses backticks by default!
[source,shell]
----
» npm run lint
----
In conclusion, there are trade-offs to consider when using template literals. You're invited to experiment with the backtick-first approach and gauge its merits. Always prefer convenience, over ((("template literals", startref="tl9")))((("template literals", "quotes and backticks", startref="tl9qab")))convention, over configuration.
=== Shorthand Notation and Object Destructuring
<> introduced us to the concept of ((("shorthand notation", id="sn9")))shorthand notation. Whenever we want to introduce a property and there's a binding by the same name in scope, we can avoid repetition.
[source,javascript]
----
const unitPrice = 1.25
const tomato = {
name: 'Tomato',
color: 'red',
unitPrice
}
----
This feature becomes particularly useful in the context of functions and information hiding. In the following example we leverage object destructuring for a few pieces of information from a grocery item and return a model that also includes the total price for the items.
[source,javascript]
----
function getGroceryModel({ name, unitPrice }, units) {
return {
name,
unitPrice,
units,
totalPrice: unitPrice * units
}
}
getGroceryModel(tomato, 4)
/*
{
name: 'Tomato',
unitPrice: 1.25,
units: 4,
totalPrice: 5
}
*/
----
Note how well shorthand notation works in tandem with destructuring. If you think of destructuring as a way of pulling properties out of an object, then you can think of shorthand notation as the analog for placing properties onto an object. The following example shows how we can leverage the `getGroceryModel` function to pull the `totalPrice` of a grocery item when we know how many the customer is buying.
[source,javascript]
----
const { totalPrice } = getGroceryModel(tomato, 4)
----
While counterintuitive at first, usage of destructuring in function parameters results in a convenient and implicitly contract-based solution, where we know that the first parameter to `getGroceryModel` is expected to be an object containing `name` and `unitPrice` properties.
[source,javascript]
----
function getGroceryModel({ name, unitPrice }, units) {
return {
name,
unitPrice,
units,
totalPrice: unitPrice * units
}
}
----
Conversely, ((("shorthand notation", startref="sn9")))destructuring a function's ((("destructuring", "objects", id="d9")))output gives the reader an immediate feel for what aspect of that output a particular piece of code is interested in. In the next snippet, we'll use only the product name and total price so that's what we destructure out of the output.
[source,javascript]
----
const { name, totalPrice } = getGroceryModel(tomato, 4)
----
Compare the last snippet with the following line of code, where we don't use destructuring. Instead, we pull the output into a `model` binding. While subtle, the key difference is that this piece communicates less information explicitly: we need to dig deeper into the code to find out which parts of the model are being used.
[source,javascript]
----
const model = getGroceryModel(tomato, 4)
----
Destructuring can also help avoid repeating references to the host object when it comes to using several properties from the same object.
[source,javascript]
----
const summary = `${ model.units }x ${ model.name }
($${ model.unitPrice }) = $${ model.totalPrice }`
// <- '4x Tomato ($1.25) = $5'
----
However, there's a trade-off here: we avoid repeating the host object when referencing properties, but at the expense of repeating property names in our destructuring declaration statement.
[source,javascript]
----
const { name, units, unitPrice, totalPrice } = model
const summary = `${ units }x ${ name } ($${ unitPrice }) =
$${ totalPrice }`
----
Whenever there are several references to the same property, it becomes clear that we should avoid repeating references to the host object, by destructuring it.
When there's a single reference to a single property, it's clear we should avoid destructuring, as it mostly generates noise.
[source,javascript]
----
const { name } = model
const summary = `This is a ${ name } summary`
----
Having a reference to `model.name` directly in the `summary` code is less noisy.
[source,javascript]
----
const summary = `This is a ${ model.name } summary`
----
When we have two properties to destructure (or two references to one property), things change a bit.
[source,javascript]
----
const summary = `This is a summary for ${ model.units }x
${ model.name }`
----
Destructuring does help in this case. It reduces the character count in the `summary` declaration statement, and it explicitly announces the `model` properties we're going to be using.
[source,javascript]
----
const { name, units } = model
const summary = `This is a summary for ${ units }x ${ name }`
----
If we have two references to the same property, similar conditions apply. In the next example, we have one less reference to `model` and one more reference to `name` than we'd have without destructuring. This case could go either way, although the value in explicitly declaring the future usage of `name` could be incentive enough to warrant destructuring.
[source,javascript]
----
const { name } = model
const summary = `This is a ${ name } summary`
const description = `${ name } is a grocery item`
----
Destructuring is as valuable as the amount of references to host objects it eliminates, but the amount of properties being referenced can dilute value, because of increased repetition in the destructuring statement. In short, destructuring is a great feature but it doesn't necessarily lead to more readable code every time. Use it judiciously, especially when there aren't that many host references being ((("destructuring", "objects", startref="d9")))removed.
=== Rest and Spread
Matches for regular expressions are represented as an array. The matched portion of the input is placed in the first position, while each captured group is placed in subsequent elements in the array. Often, we are interested in specific captures such as the first one.
In the following example, ((("destructuring", "arrays")))((("arrays")))array destructuring helps us omit the whole match and place the `integer` and `fractional` parts of a number into corresponding variables. This way, we avoid resorting to magic numbers pointing at the indices where captured groups will reside in the match result.
[source,javascript]
----
function getNumberParts(number) {
const rnumber = /(\d+)\.(\d+)/
const matches = number.match(rnumber)
if (matches === null) {
return null
}
const [ , integer, fractional] = number.match(rnumber)
return { integer, fractional }
}
getNumberParts('1234.56')
// <- { integer: '1234', fractional: '56' }
----
The ((("spread operator", id="so9")))spread operator could be used to pick up every captured group, as part of destructuring the result of `.match`.
[source,javascript]
----
function getNumberParts(number) {
const rnumber = /(\d+)\.(\d+)/
const matches = number.match(rnumber)
if (matches === null) {
return null
}
const [ , ...captures] = number.match(rnumber)
return captures
}
getNumberParts('1234.56')
// <- ['1234', '56']
----
When we need to concatenate lists, we use `.concat` to create a new array. The spread operator improves code readability by making it immediately obvious that we want to create a new collection comprising each list of inputs, while preserving the ease of adding new elements declaratively in array literals.
[source,javascript]
----
administrators.concat(moderators)
[...administrators, ...moderators]
[...administrators, ...moderators, bob]
----
Similarly, the object spread featurefootnote:[Currently in stage 3 of the ECMAScript standard development process.] introduced in <> allows us to merge objects onto a new object. Consider the following snippet where we programmatically create a new object comprising base `defaults`, user-provided `options`, and some important override property that prevails over previous properties.
[source,javascript]
----
Object.assign({}, defaults, options, { important: true })
----
Compare that to the equivalent snippet using object spread declaratively. We have the object literal, the `defaults` and `options` being spread, and the `important` property. Not using the `Object.assign` function has greatly improved our code's readability, even letting us inline the `important` property in the object literal declaration.
[source,javascript]
----
{
...defaults,
...options,
important: true
}
----
Being able to visualize object spread as an `Object.assign` helps internalize how the feature works. In the following example we've replaced the `defaults` and `options` variables with object literals. Since object spread relies on the same operation as `Object.assign` for every property, we can observe how the `options` literal overrides `speed` with the number `3`, and why `important` remains `true` even when the `options` literal attempts to override it, due to precedence.
[source,javascript]
----
{
...{ // defaults
speed: 1,
type: 'sports'
},
...{ // options
speed: 3,
important: false
},
important: true
}
----
Object spread ((("object spread")))comes in handy when we're dealing with immutable structures, where we're supposed to create new objects instead of editing existing ones. Consider the following bit of code where we have a `player` object and a function call that casts a healing spell and returns a new, healthier, +player+ object.
[source,javascript]
----
const player = {
strength: 4,
luck: 2,
mana: 80,
health: 10
}
castHealingSpell(player) // consumes 40 mana, gains 110 health
----
The following ((("spread operator", startref="so9")))snippet shows an implementation of `castHealingSpell` where we create a new `player` object without mutating the original `player` parameter. Every property in the original `player` object is copied over, and we can update individual properties as needed.
[source,javascript]
----
const castHealingSpell = player => ({
...player,
mana: player.mana - 40,
health: player.health + 110
})
----
As we explained in <>, we can use object rest properties while destructuring objects. Among other uses, such as listing unknown properties, object rest can be used to create a shallow copy of an object.
In the next snippet, we'll look at three of the simplest ways in which we can create a shallow copy of an object in JavaScript. The first one uses `Object.assign`, assigning every property of `source` to an empty object that's then returned; the second example uses object spread and is equivalent to using `Object.assign`, but a bit more gentle on the eyes; the last example relies on destructuring the rest parameter.
[source,javascript]
----
const copy = Object.assign({}, source)
const copy = { ...source }
const { ...copy } = source
----
Sometimes we need to create a copy of an object, but omit some properties in the resulting copy. For instance, we may want to create a copy of `person` while omitting their `name`, so that we only keep their metadata.
One way to achieve that with plain JavaScript would be to destructure the `name` property while placing other properties in a `metadata` object, using the rest parameter. Even though we don't need the `name`, we've effectively "removed" that property from the `metadata` object, which contains the rest of the properties in `person`.
[source,javascript]
----
const { name, ...metadata } = person
----
In the following bit of code, we map a list of people to a list of `person` models, excluding personally identifiable information such as their name and Social Security number, while placing everything else in the `person` rest parameter.
[source,javascript]
----
people.map(({ name, ssn, ...person }) => person)
----
=== Savoring Function Flavors
JavaScript already offered a number of ways in which we can declare functions before ES6.
Function declarations ((("function declarations")))are the most prominent kind of JavaScript function. The fact that declarations are hoisted means we can sort them based on how to improve code readability, instead of worrying about sorting them in the exact order they are used.
The following snippet displays three function declarations arranged in such a way that the code is more linear to read.
[source,javascript]
----
printSum(2, 3)
function printSum(x, y) {
return print(sum(x, y))
}
function sum(x, y) {
return x + y
}
function print(message) {
console.log(`printing: ${ message }`)
}
----
Function expressions, ((("function expressions", id="fe9")))in contrast, must be assigned to a variable before we can execute them. Keeping with the preceding example, this means we would necessarily need to have all function expressions declared before any code can use them.
The next snippet uses function expressions. Note that if we were to place the `printSum` function call anywhere other than after all three expression assignments, our code would fail because of a variable that hasn't been initialized yet.
[source,javascript]
----
var printSum = function (x, y) {
return print(sum(x, y))
}
var sum = function (x, y) {
return x + y
}
// a `printSum()` statement would fail: print is not defined
var print = function (message) {
console.log(`printing: ${ message }`)
}
printSum(2, 3)
----
For this reason, it may be better to sort function expressions as a LIFO (last-in-first-out) stack: placing the last function to be called first, the second to last function to be called second, and so on. The rearranged code is shown in the next snippet.
[source,javascript]
----
var sum = function (x, y) {
return x + y
}
var print = function (message) {
console.log(`printing: ${ message }`)
}
var printSum = function (x, y) {
return print(sum(x, y))
}
printSum(2, 3)
----
While this code is a bit harder to follow, it becomes immediately obvious that we can't call `printSum` before the function expression is assigned to that variable. In the previous piece of code this wasn't obvious because we weren't following the LIFO rule. This is reason enough to prefer function declarations for the vast majority of our code.
Function expressions can have a name that can be used for recursion, but that name is not accessible in the outer scope. The following example shows a function expression that's named `sum` and assigned to a `sumMany` variable. The `sum` reference is used for recursion in the inner scope, but we get an error when trying to use it from the outer scope.
[source,javascript]
----
var sumMany = function sum(accumulator = 0, ...values) {
if (values.length === 0) {
return accumulator
}
const [value, ...rest] = values
return sum(accumulator + value, ...rest)
}
console.log(sumMany(0, 1, 2, 3, 4))
// <- 10
console.log(sum())
// <- ReferenceError: sum is not defined
----
Arrow ((("arrow functions", id="af9")))functions, introduced in <>, are similar to function expressions. The syntax is made shorter by dropping the `function` keyword. In arrow functions, parentheses around the parameter list are optional when there's a single parameter that's not destructured nor the rest parameter. It is possible to implicitly return any valid JavaScript expression from an arrow function without declaring a block statement.
The following snippet shows an arrow function explicitly returning an expression in a block statement, one that implicitly returns the expression, one that drops the parentheses around its only parameter, and one that uses a block statement but doesn't return a value.
[source,javascript]
----
const sum = (x, y) => { return x + y }
const multiply = (x, y) => x * y
const double = x => x * 2
const print = x => { console.log(x) }
----
Arrow functions can return arrays using tiny expressions. The first example in the next snippet implicitly returns an array comprising two elements, while the second example discards the first parameter and returns all other parameters held in the rest operator's bag.
[source,javascript]
----
const makeArray = (first, second) => [first, second]
const makeSlice = (discarded, ...items) => items
----
Implicitly returning an object literal is a bit tricky because they're hard to tell apart from block statements, which are also wrapped in curly braces. We'll have to add parentheses around our object literal, turning it into an expression that evaluates into the object. This bit of indirection is just enough to help us disambiguate and tell JavaScript parsers that they're dealing with an object literal.
Consider the following example, where we implicitly return an object expression. Without the parentheses, the parser would interpret our code as a block statement containing a label and the literal expression `'Nico'`.
[source,javascript]
----
const getPerson = name => ({
name: 'Nico'
})
----
Explicitly naming arrow functions isn't possible, due to their syntax. However, if an arrow function expression is declared in the righthand side of a variable or property declaration, then its name becomes the name for the arrow function.
Arrow function expressions need to be assigned before use, and thus suffer from the same ordering ailments as regular function expressions. In addition, since they can't be named, they must be bound to a variable for us to reference them in recursion scenarios.
Using function declarations by default should be preferred. They are less limited in terms of how they can be ordered, referenced, and executed, leading to better code readability and maintainability. In future refactors, we won't have to worry about keeping function declarations in the same order in fear of breaking dependency chains or LIFO representations.
That said, arrow functions are a terse and powerful way of declaring functions in short form. The smaller the function, the more valuable using arrow syntax becomes, as it helps avoid a situation where we spend more code on form than we spend on function. As a function grows larger, writing it in arrow form loses its appeal due to the aforementioned ordering and naming issues.
Arrow functions are invaluable in cases where we would've otherwise declared an anonymous function expression, such as in test cases, functions passed to `new Promise()` and `setTimeout`, or array mapping functions.
Consider the following example, where we use a nonblocking `wait` promise to print a statement after five seconds. The `wait` function takes a `delay` in milliseconds and returns a `Promise`, which resolves after waiting for the specified time with `setTimeout`.
[source,javascript]
----
wait(5000).then(function () {
console.log('waited 5 seconds!')
})
function wait(delay) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve()
}, delay)
})
}
----
When switching to arrow functions, we should stick with the top-level `wait` function declaration ((("function declarations")))so that we don't need to hoist it to the top of our scope. We can turn every other function into arrows to improve readability, thus removing many `function` keywords that got in the way of interpreting what those functions do.
The next snippet shows what that code would look like using arrow functions. With all the keywords out of the way after refactoring, it's easier to understand the relationship between the `delay` parameter of `wait` and the second argument to `setTimeout`.
[source,javascript]
----
wait(5000).then(
() => console.log('waited 5 seconds!')
)
function wait(delay) {
return new Promise(resolve =>
setTimeout(() => resolve(), delay)
)
}
----
Another large upside in using arrow functions lies in their lexical scoping, where they don't modify the meaning of `this` or `arguments`. If we find ourselves copying `this` to a temporary variable--typically named `self`, `context`, or ++_this++—we may want to use an arrow function for the inner bit of code instead. Let's take a look at an example of this.
[source,javascript]
----
const pistol = {
caliber: 50,
trigger() {
const self = this
setTimeout(function () {
console.log(`Fired caliber ${ self.caliber } pistol`)
}, 1000)
}
}
pistol.trigger()
----
If we tried to use `this` directly in the previous example, we'd get a caliber of `undefined` instead. With an arrow function, however, we can avoid the temporary `self` variable. We not only removed the `function` keyword but we also gained functional value due to lexical scoping, since we don't need to work our way around the language's limitations anymore in this case.
[source,javascript]
----
const pistol = {
caliber: 50,
trigger() {
setTimeout(() => {
console.log(`Fired caliber ${ this.caliber } pistol`)
}, 1000)
}
}
pistol.trigger()
----
As a general rule of thumb, think of every function as a function declaration by default. If that function doesn't need a meaningful name, requires several lines of code, or involves recursion, ((("arrow functions", startref="af9")))((("function expressions", startref="fe9")))then consider an arrow function.
=== Classes and Proxies
Most modern programming ((("classes", id="class9")))languages have classes in one form or another. JavaScript classes are syntactic sugar on top of prototypal inheritance. Using classes turns prototypes more idiomatic and easier for tools to statically analyze.
When writing prototype-based solutions the constructor code is the function itself, while declaring instance methods involves quite a bit of boilerplate code, as shown in the following code snippet.
[source,javascript]
----
function Player() {
this.health = 5
}
Player.prototype.damage = function () {
this.health--
}
Player.prototype.attack = function (player) {
player.damage()
}
----
In contrast, classes normalize the `constructor` as an instance method, thus making it clear that the constructor is executed for every instance. At the same time, methods are built into the `class` literal and rely on a syntax that's consistent with methods in object literals.
[source,javascript]
----
class Player {
constructor() {
this.health = 5
}
damage() {
this.health--
}
attack(player) {
player.damage()
}
}
----
Grouping instance methods under an object literal ensures class declarations aren't spread over several files, but rather unified in a single location describing their whole API.
Declaring any `static` methods as part of a `class` literal, as opposed to dynamically injecting them onto the class, also helps centralize API knowledge. Keeping this knowledge in a central location helps code readability because developers need to go through less code to learn the `Player` API. At the same time, when we define a convention of declaring instance and static methods on the `class` literal, coders know not to waste time looking elsewhere for methods defined dynamically. The same applies to getters and setters, which we can also define on the `class` literal.
[source,javascript]
----
class Player {
constructor() {
Player.heal(this)
}
damage() {
this.health--
}
attack(player) {
player.damage()
}
get alive() {
return this.health > 0
}
static heal(player) {
player.health = 5
}
}
----
Classes also offer `extends`, simple syntactic sugar on top of prototypal inheritance. This, again, is more convenient than prototype-based solutions. With `extends`, we don't have to worry about choosing a library or otherwise dynamic method of inheriting from another class.
[source,javascript]
----
class GameMaster extends Player {
constructor(...rest) {
super(...rest)
this.health = Infinity
}
kill(player) {
while (player.alive) {
player.damage()
}
}
}
----
Using that same syntax, classes can extend native built-ins such as `Array` or `Date` without relying on an `