

Copyright © 2020 Packt Publishing
All rights reserved. No part of this book may be reproduced, stored in a retrieval system, or transmitted in any form or by any means, without the prior written permission of the publisher, except in the case of brief quotations embedded in critical articles or reviews.
Every effort has been made in the preparation of this book to ensure the accuracy of the information presented. However, the information contained in this book is sold without warranty, either express or implied. Neither the author, nor Packt Publishing or its dealers and distributors, will be held liable for any damages caused or alleged to have been caused directly or indirectly by this book.
Packt Publishing has endeavored to provide trademark information about all of the companies and products mentioned in this book by the appropriate use of capitals. However, Packt Publishing cannot guarantee the accuracy of this information.
Commissioning Editor: Pavan Ramchandani
Acquisition Editor: Ashitosh Gupta
Content Development Editor: Akhil Nair
Senior Editor: Martin Whittemore
Technical Editor: Suwarna Patil
Copy Editor: Safis Editing
Project Coordinator: Kinjal Bari
Proofreader: Safis Editing
Indexer: Manju Arasan
Production Designer: Deepika Naik
First published: January 2020
Production reference: 1170120
Published by Packt Publishing Ltd.
Livery Place
35 Livery Street
Birmingham
B3 2PB, UK.
ISBN 978-1-78995-764-8

Subscribe to our online digital library for full access to over 7,000 books and videos, as well as industry leading tools to help you plan your personal development and advance your career. For more information, please visit our website.
Spend less time learning and more time coding with practical eBooks and Videos from over 4,000 industry professionals
Improve your learning with Skill Plans built especially for you
Get a free eBook or video every month
Fully searchable for easy access to vital information
Copy and paste, print, and bookmark content
Did you know that Packt offers eBook versions of every book published, with PDF and ePub files available? You can upgrade to the eBook version at www.packt.com and as a print book customer, you are entitled to a discount on the eBook copy. Get in touch with us at customercare@packtpub.com for more details.
At www.packt.com, you can also read a collection of free technical articles, sign up for a range of free newsletters, and receive exclusive discounts and offers on Packt books and eBooks.
James Padolsey is a passionate JavaScript and UI engineer with over 12 years' experience. James began his journey into JavaScript as a teenager, teaching himself how to build websites for school and small freelance projects. In the early years, he was a prolific blogger, sharing his unique solutions to common problems in the domains of jQuery, JavaScript, and the DOM. He later contributed to the jQuery library itself and authored a chapter within the jQuery Cookbook published by O'Reilly Media. Over subsequent years, James has been exposed to many unique software projects in his employment at Stripe, Twitter, and Facebook, informing his philosophy on what clean coding truly means in the ever-changing ecosystem of JavaScript.
Derrek Landauer teaches middle school math and mentors students in an Air Force Research Lab rocketry science program. He earned a bachelor of science in electrical engineering from the University of Texas at El Paso in 2011. His work history spans industry and academia. While attending school, he was involved in defense research projects and was also a lab instructor. He later spent a couple of years managing the network and server infrastructure across four facilities for a subsidiary of a Fortune 100 company. His software development background ranges from programming microprocessors and operating systems to full stack web development.
Dobrin Ganev is a software developer with years of experience working in various development environments, ranging from finance to business process management. In recent years, he has focused on geospatial development and data analytics using JavaScript, Python, Scala, and R. He has extensive knowledge of open source geospatial software and the ESRI platform. He is also skilled in Node.js, React.js, and GraphQL. Dobrin recently authored a video course entitled Hands-On Full Stack Web Development with GraphQL and React, published by Packt.
If you're interested in becoming an author for Packt, please visit authors.packtpub.com and apply today. We have worked with thousands of developers and tech professionals, just like you, to help them share their insight with the global tech community. You can make a general application, apply for a specific hot topic that we are recruiting an author for, or submit your own idea.
JavaScript is a scrappy yet graceful language that has found itself at the center of one of the greatest software shifts in history. It is now the primary programming language used to deliver user experiences on the most ubiquitous platform that exists: the web.
This huge responsibility has meant that the JavaScript language has had to grow up very quickly in a period of shifting demands. For the up-and-coming JavaScript programmer or web developer, these changes have meant that the language and its ecosystem have been increasingly complex to grasp. Nowadays, the sheer number of frameworks and libraries available is overwhelming, even to those who've been in the industry for many years.
The task of this book is to peel back the confusing layers and concepts that the world has placed atop the language to reveal its underlying nature and consider how we can use it to craft reliable and maintainable code with a focus on usability. We will begin by zooming out and considering, in a very fundamental way, why we even write code. We will discover that the code we write does not exist in a vacuum. We will explore the large and small ways in which our code drastically affects our users and fellow programmers, and discuss ways that we can accommodate their various needs.
Beyond a fundamental exploration of clean coding principles, we will deeply delve into JavaScript itself, guiding you through the language, from its most atomic syntax to its more abstract design patterns and conventions. We will also explore how we can go about documenting and testing our code in the cleanest way possible. You should come away with a solid grasp of the JavaScript language and an attuned sense of what clean code is.
This book is for anyone who has an interest in improving their JavaScript skills. Whether you are an amateur or a professional, there are aspects of this book that you will find valuable. In terms of technical knowledge, the book assumes some previous exposure to programming and at least a small amount of experience of JavaScript itself. The reader who will get the most value from this book is someone who has programmed for a number of months or years in JavaScript but has always felt weighed down by the complexity of it and is unsure of how to craft clean and bug-free JavaScript.
Chapter 1, Setting the Scene, asks you to consider why we write code and explores the many ways in which we communicate our intent via code. This chapter provides a firm foundation upon which you can build and adapt your understanding of clean code.
Chapter 2, The Tenets of Clean Code, uses real-world JavaScript examples to explore the four tenets of clean code: reliability, efficiency, maintainability, and usability. Each of these vital tenets serves as a foundation for the rest of the book.
Chapter 3, The Enemies of Clean Code, uncovers some of the more notorious enemies of clean code. These are the forces and dynamics that lead to the proliferation of unclean code, such as egotistic programming, bad metrics, and cargo cults.
Chapter 4, SOLID and Other Principles, explores the famous SOLID principles and uncovers their deeper meaning by tying them together with functional programming principles, the Law of Demeter, and the abstraction principle.
Chapter 5, Naming Things Is Hard, discusses one of the most challenging aspects of programming: naming things. It poses some of the specific challenges of naming and ties together a foundational naming theory with real-world naming problems and solutions.
Chapter 6, Primitive and Built-In Types, begins a deep exploration into JavaScript. This chapter details the primitive and built-in types available to the JavaScript programmer, warning against common pitfalls and sharing best practices.
Chapter 7, Dynamic Typing, discusses JavaScript's dynamic nature, and goes over some of the challenges related to this. It explains how we can both cleanly detect and convert to various types (via explicit casting or implicit coercion).
Chapter 8, Operators, thoroughly details the operators available within JavaScript, discussing their behaviors and challenges. This includes a detailed account of every operator alongside examples, pitfalls, and best practices.
Chapter 9, Parts of Syntax and Scope, provides a more macro view of the language, highlighting the broader syntaxes and constructs available, such as statements, expressions, blocks, and scope.
Chapter 10, Control Flow, broadly covers the concept of control flow, highlighting the crucial difference between imperative and declarative forms of programming. It then explores how we can cleanly control flow within JavaScript by utilizing control-moving mechanisms such as invoking, returning, yielding, throwing, and more.
Chapter 11, Design Patterns, broadly explores some of the more popular design patterns used in JavaScript. It describes the major architectural design patterns of MVC and MVVM, and the more modular design patterns such as the Constructor pattern, the Class pattern, the Prototype pattern, and the Revealing Module pattern.
Chapter 12, Real-World Challenges, looks at some of the more realistic problem domains within the JavaScript ecosystem and considers how they can be handled cleanly. Topics covered include the DOM and single-page applications, dependency management, and security (XSS, CSRF, and more).
Chapter 13, The Landscape of Testing, describes the broad concepts of testing software, and how these can be applied to JavaScript. It specifically explores unit testing, integration testing, E2E testing, and TDD.
Chapter 14, Writing Clean Tests, delves further into the domain of testing by advising you to author assertions and test suites in a way that is utterly clear, intuitive, representative of the problem domain, and conceptually hierarchical.
Chapter 15, Tools for Cleaner Code, briefly considers several available tools and development flows that can greatly aid us in writing and maintaining clean code. Included are topics such as linting, formatting, source control, and continuous integration.
Chapter 16, Documenting Your Code, uncovers the unique challenges of documentation. This chapter challenges you to consider all the mediums of documentation that are available and asks you to consider how we can understand and accommodate the needs and questions of individuals who may wish to utilize or maintain our code.
Chapter 17, Other Peoples' Code, looks into the challenges of selecting, understanding, and making use of third-party code within our JavaScript projects (such as third-party libraries, frameworks, and utilities). It also discusses methods of encapsulation that allow us to interface with third-party code in a clean and minimally invasive way.
Chapter 18, Communication and Advocacy, explores the wider project-based and interpersonal challenges inherent in the crafting and delivery of clean software. This includes a detailed inquiry into the following: planning and setting requirements, communication strategies, and identifying issues and driving change.
Chapter 19, Case Study, concludes the book with a walk-through of the development of a JavaScript project, including both client-side and server-side pieces. This chapter draws together the principles espoused within the book and affirms them by exposing you to a real-world problem domain and the development of a usable solution.
In order to get the most out of this book, it is useful to have a basic understanding of the JavaScript language and to have some experience of atleast one platform in which JavaScript is utilized. This may include the browser or Node.js, for example.
In order for you to execute the pieces of code shared within the book, you have a few options available:
Browser development tools are accessible within all modern browsers. The shortcuts are as follows:
You are advised to move through the book at your own pace and conduct additional research and exploration online if you are finding a topic hard to grasp. Some especially helpful resources include the following:
The book gets progressively more detailed as you advance through it, so it is natural to take a slower pace in later chapters. This may be especially true for Chapters 6-12, which cover, in great detail, the characteristics of the JavaScript language itself.
You can download the example code files for this book from your account at www.packt.com. If you purchased this book elsewhere, you can visit www.packtpub.com/support and register to have the files emailed directly to you.
You can download the code files by following these steps:
Once the file is downloaded, please make sure that you unzip or extract the folder using the latest version of:
The code bundle for the book is also hosted on GitHub at https://github.com/PacktPublishing/Clean-Code-in-JavaScript. In case there's an update to the code, it will be updated on the existing GitHub repository.
We also have other code bundles from our rich catalog of books and videos available at https://github.com/PacktPublishing/. Check them out!
We also provide a PDF file that has color images of the screenshots/diagrams used in this book. You can download it here: https://static.packt-cdn.com/downloads/9781789957648_ColorImages.pdf.
There are a number of text conventions used throughout this book.
CodeInText: Indicates code words in text, database table names, folder names, filenames, file extensions, pathnames, dummy URLs, user input, and Twitter handles. Here is an example: "We find a publicly available package called shipping_address_validator and decide to use it."
A block of code is set as follows:
function validatePostalCode(code) {
return /^[0-9]{5}(?:-[0-9]{4})?$/.test(code);
}
Any command-line input or output is written as follows:
npm install --save react react-dom
Bold: Indicates a new term, an important word, or words that you see on screen. For example, words in menus or dialog boxes appear in the text like this. Here is an example: "For the purposes of our case study, the plant names only exist as their full Latin names, which includes a family (for example, Acanthaceae)."
Feedback from our readers is always welcome.
General feedback: If you have questions about any aspect of this book, mention the book title in the subject of your message and email us at customercare@packtpub.com.
Errata: Although we have taken every care to ensure the accuracy of our content, mistakes do happen. If you have found a mistake in this book, we would be grateful if you would report this to us. Please visit www.packtpub.com/support/errata, selecting your book, clicking on the Errata Submission Form link, and entering the details.
Piracy: If you come across any illegal copies of our works in any form on the Internet, we would be grateful if you would provide us with the location address or website name. Please contact us at copyright@packt.com with a link to the material.
If you are interested in becoming an author: If there is a topic that you have expertise in and you are interested in either writing or contributing to a book, please visit authors.packtpub.com.
Please leave a review. Once you have read and used this book, why not leave a review on the site that you purchased it from? Potential readers can then see and use your unbiased opinion to make purchase decisions, we at Packt can understand what you think about our products, and our authors can see your feedback on their book. Thank you!
For more information about Packt, please visit packt.com.
In this section, we'll discuss the purpose of code and the tenets of it, such as clarity and maintainability. We'll also cover the very broad challenge of naming things, as well as some of the valuable questions and hazards to watch out for.
This section contains the following chapters:
JavaScript was created by Brendan Eich in 1995, with the goal of being a glue language. It was intended to help web designers and amateurs easily manipulate and derive behavior from their HTML. JavaScript was able to do this via the DOM API, a set of interfaces provided by the browser that would give access to the parsed representation of HTML. Soon after this, DHTML became the popular term, referring to the more dynamic user interfaces that JavaScript enabled: everything from animated rollover button states to client-side form validation. Eventually came the rise of Ajax, which enabled communication between the client and the server. This opened up a considerable fountain of potential applications. The web, previously purely the domain of documents, was now on the way to becoming a powerhouse of processor- and memory-intensive applications:

In 1995, nobody could have predicted that JavaScript would one day be used to build complex web applications, program robots, query databases, write plugins for photo manipulation software, and be behind one of the most popular server runtimes in existence, Node.js.
In 1997, not long after its creation, JavaScript was standardized by Ecma International under the name ECMAScript, and it is still undergoing frequent changes under the TC39 committee. Most recent versions of the language have been named according to the year of their release, such as ECMAScript 2020 (ES2020).
Due to its burgeoning capabilities, JavaScript has attracted a passionate community that drives its growth and ubiquity. And due to its considerable popularity, there are now countless different ways to do the same thing in JavaScript. There are thousands of popular frameworks, libraries, and utilities. The language too is changing on a near-constant basis in reaction to the increasing demands of its applications. This creates a great challenge: among all of this change, while being pushed and pulled in different directions, how can we know how to write the best possible code? Which frameworks should we use? What conventions should we employ? How should we test our code? How should we craft sensible abstractions?
To answer these questions, we need to briefly go back to basics. And that is the purpose of this chapter. We'll be discussing the following:
At its simplest, we know that programming is about instructing computers, but what are we instructing them to do? And to what end? And what other purposes does code serve?
We can broadly say that code is a way of solving problems. By writing code, we are expressing a complex task or series of actions, distilling them into a singular process that can be easily utilized by a user. So we can say that the code is an expression of a problem domain. We can even say it is a form of communication, a way to relay information and intent. Understanding that code is a complex thing with many complementary purposes, such as problem-solving and communication, will enable us to use it to its fullest potential. Let's delve further into this complexity by exploring what we mean when we speak of code as a method of relaying intent.
We often think of code as simply a series of instructions that are executed by a computer. But in many ways, this misses the true magic of what we're doing when we write code. When we convey instructions, we are expressing our intent to the world; we are saying These are the things that I want to occur.
Humans have been conveying instructions for as long as they've been around. One example of this is a simple cooking recipe:
Instructions like these are quite easy to understand for a human, but you'll notice they follow no strict specification. The measuring units are inconsistent, as is the punctuation and the wording. And some of the instructions are quite ambiguous and therefore open to misinterpretation by someone who hasn't cooked before:
Humans can usually muddle through such ambiguities with their initiative and experience, but machines aren't so adept. A machine must be instructed with enough specificity to carry out every step. What we wish to communicate to a machine is our intent, that is, please do this thing, but due to the nature of machines, we must be utterly specific. Thankfully, how we choose to write these instructions is up to us; there are many programming languages and approaches, and almost all of them were created with the goal of making it easier for humans to communicate their intent in a less burdensome way.
The distance between human capability and computing capability is quickly narrowing. The advent of machine learning, natural language processing, and highly specialized programs means that machines are far more flexible in the types of instructions they can carry out. However, code will continue to be useful for some time, as it allows us to communicate in a highly specific and standardized way. With this high level of specificity and consistency, we can have more faith that our instructions will be executed as intended, every time.
No meaningful conversation about programming can occur without considering the user. The user, whether they are a fellow programmer or the end user of a UI, is at the core of what we do.
Let's imagine that we are tasked with validating user-inputted shipping addresses on a website. This particular website sells medication to hospitals around the world. We're in a bit of a rush and would prefer to use something that someone else has implemented. We find a publicly available package called shipping_address_validator and decide to use it.
If we had taken the time to check the code within the package, in its postal code validation file, we would have seen this:
function validatePostalCode(code) {
return /^[0-9]{5}(?:-[0-9]{4})?$/.test(code);
}
Unfortunately, due to our haste, we didn't question the functionality of the shipping_address_validator package. We assumed it did what it says on the tin. One week after releasing our code to production we get a bug report saying that some users are unable to enter their address information. We look at the code and realize, to our horror, that it only validates US ZIP codes, not all countries' postal codes (for example, it doesn't work on UK postcodes, such as GR82 5JY).
Through this unfortunate series of events, this piece of code is now responsible for blocking the shipment of vital medication to customers all over the world, numbering in the thousands. Fortunately, fixing it doesn't take too long.
Forgetting for a moment who is responsible for this mishap, I'd like to pose the following question: who are the users of this code?
There isn't a clear-cut answer to this question. When bugs appear in the code, we can see how there can be massive unfortunate downstream effects. Should the original programmer of the package be concerned with all these downstream dependencies? When a plumber is hired to fix a tap on a sink, should they only consider the function of the tap itself, or the sink into which it pours?
When we write code, we are defining an implicit specification. This specification is communicated by its name, its configuration options, its inputs, and its outputs. Anyone who uses our code has the right to expect it to work according to its specifications, so the more explicit we can be, the better. If we're writing code that only validates US ZIP codes, then we should name it accordingly. When people create software atop our code, we can't have any control over how they use it. But we can communicate explicitly about it, ensuring that its functionality is clear and expected.
It's important to consider all use cases of our code, to imagine how it might be used and what expectations humans will have about it, programmers and end users alike. What we are responsible or accountable for is up for debate, and is as much a legal question as a technical one. But the question of who our users are is entirely up to us. In my experience, the better programmers consider the full gamut of users, aware that the software they write does not exist in a vacuum.
We've spoken about the importance of the user in programming, and how we must first understand what it is they wish to do if we are to have any hope of helping them.
Only by understanding the problem can we begin to assemble requirements that our code will have to fulfill. In the exploration of the problem, it's useful to ask yourself the following questions:
When we have assembled a complete understanding of the problem, we can then begin ideating, planning, and writing code to solve it. At each step, often without realizing it, we will be modeling the problem in a way that makes sense to us. The way we think about the problem will have a drastic effect on the solution we end up creating. The model of the problem we create will dictate the code we end up writing.
Let's imagine for a moment that we are responsible for a note-taking application for students and are tasked with creating a solution to the following problem that a user has expressed:
We've decided that this warrants changes to the software because we've heard similar things from other users. So, we sit down and try to come up with various ideas for how we could improve the organization of notes. There are a few options we could explore:
Each solution has its pros and cons, and there is also the possibility of implementing a combination of them. One thing that becomes immediately obvious is that each of these will quite drastically affect how users end up using the application. We can imagine how users exposed to these respective solutions would hold the model of note-taking in their minds:
In this example, we're developing a UI, so we are sitting very close to the end user of the application. However, the modeling of problems is applicable to all of the work we do. If we were creating a pure REST API for note-keeping, exactly the same considerations would need to be made. Web programmers play a key part in deciding what models other people end up employing. We should not take this responsibility lightly.
The first point of failure is typically misunderstanding the problem. If we don't understand what users are truly trying to accomplish, and we have not received all requirements, then we will inevitably retain a bad model of the problem and thus end up implementing the wrong solutions.
Imagine that this scenario occurs at some point before the invention of the kettle:
Matthew asks no questions and immediately gets to work, excited at the prospect of putting his creativity to use. One day later he comes up with the following contraption:

We can see, quite obviously, that Matthew has forgotten one key component. In his haste, he did not stop to ask Susanne for more information about the user, or about their problem, and so did not consider the eventuality that a user would need to pick up the boiling-hot vessel somehow. After receiving feedback, naturally, he designed and introduced a handle to the kettle:

This needn't have occurred at all, though. Imagine this kettle scenario extrapolated to the complexity and length of a large software project spanning multiple months. Imagine the headaches and needless pain involved in such a misunderstanding. The key to designing a good solution to a problem requires, first and foremost, a correct and complete model of the problem. Without this, we'll fail before we even begin. This matters in the design of massive projects but also in the implementation of the smallest JavaScript utilities and components. In every line of code we write, in fact, we are utterly liable to failure if we do not first understand the problem domain.
The problem domain encapsulates not only the problem being encountered by the user but also the problem of meeting their needs via the technologies we have available. And so, the problem domain of writing JavaScript in the browser, for example, includes the complexity of HTTP, the browser object model, the DOM, CSS, and a litany of other details. A good JavaScript programmer has to be adept not only in these technologies but also in understanding new domains of problems encountered by their users.
This entire book is concerned with teaching you how to write clean code in JavaScript. In the following chapters, we'll go into a lot of detail, with discussions of almost every construct within the language. Firstly, we need to establish a few key perspectives that'll be important when we think about what it means to write clean code for humans.
We can say that writing code for humans is broadly about the clarity of intent. And writing code for machines is broadly about functionality. These needs do cross over, of course, but it's vital to discern the difference. We can see the difference if we were writing code only for the machine, focusing purely on function, and forgetting the human audience. Here's an example:
function chb(d,m,y) {
return new Date(y,m-1,d)-new Date / 6e4 * 70;
}
Do you understand what this code is doing? You may be able to decipher what's going on in this code, but it is intent—its true meaning—will be almost impossible to discern.
If we clearly express our intent then the preceding code would look something like this:
const AVG_HEART_RATE_PER_MILLISECOND = 70 / 60000;
function calculateHeartBeatsSinceBirth(birthDay, birthMonth, birthYear) {
const birthMonthIndex = birthMonth - 1;
const birthDate = new Date(birthYear, birthMonthIndex, birthDay);
const currentDate = new Date();
return (currentDate - birthDate) / AVG_HEART_RATE_PER_MILLISECOND;
}
From the preceding code, we can discern that this function is intended to calculate the number of times a heart has beaten since birth. There is no functional difference between these two pieces of code. However, the latter code better communicates the programmer's intentions, and thus is easier to understand and to maintain.
The code we write is primarily for people. You may be building a brochure website, programming a web application, or crafting a complex utility function for a framework. All of these things are for people: people who are the end users of GUIs driven by our code or people who are the programmers making use of our abstractions and interfaces. Programmers are in the business of helping these people.
Even if you're writing code only for yourself, with no possibility of it being used in any way by anyone else, your future self will thank you if you write clear code.
When we write code, it's essential to consider how human brains will consume it. Fellow programmers will scan over your code, reading the pertinent parts, attempting to gain a running comprehension of its inner workings. Readability is the first hurdle that they must overcome. If they are unable to read and cognitively navigate the code you've written then they'll be less able to use it. This will drastically limit the utility and value of your code.
Programmers, in my experience, don't tend to like thinking of code in terms of aesthetic design, but the best programmers will appreciate that these concepts are intrinsically intertwined. The design of our code in a presentational or visual sense is as vital to its comprehensibility as its architectural design. Design, in the end, is about creating something in a way that optimally delivers a purpose for its users. For our fellow programmers, that purpose is comprehension. And so we must design our code to deliver that purpose.
Machines care purely about specifications and will parse valid code into its parts with little effort. Humans, however, are more complex. We are less capable in areas where machines excel, hence their existence, but we are also skillful in areas where machines may falter. Our highly evolved brains, among their many talents, have become incredibly skilled at spotting patterns and inconsistencies. We rely on difference, or contrast, to focus our attention. If a pattern is not being followed then it creates more work for our brains. For an example of such inconsistency, have a look at this code:
var TheName='James' ;
var City = 'London'
var hobby = 'Photography',job='Programming'
You probably don't enjoy looking at this code. Its messiness is distracting and it appears to follow no particular pattern. The naming and spacing are inconsistent. Our brains struggle with this, and so reading the code, and building a full understanding of it, becomes more cognitively expensive.
We might refactor the preceding code to be more consistent, like so:
var name = 'James';
var city = 'London';
var hobby = 'Photography';
var job = 'Programming';
Here, we've used a single naming pattern and have employed consistent syntax and spacing in every statement.
Alternatively, perhaps we would like to declare all variables within a single var declaration and align the assignment (=) operators so that all values start along the same vertical axis:
var name = 'James',
city = 'London',
hobby = 'Photography',
job = 'Programming';
You'll notice that these different styles are very subjective. Some people prefer one way. Other people prefer another way. And that's okay. I am not stating which approach is superior. Instead, I am pointing out that if we care about writing code for humans, then we should care, first and foremost, about its readability and presentation, and consistency is a key part of that.
As we write code, we use and create abstractions constantly. Abstraction is what occurs when we take a piece of complexity and then present access to that complexity in a simpler way. By doing this, we enable people to have leverage over that complexity without having to wield a full understanding of it. This idea underpins most modern technology:

JavaScript, like many other high-level languages, presents an abstraction that enables us not to have to worry about the details of how a computer operates. We can, for example, ignore the problem of memory allocation. Even though we must be sensitive to the constraints of hardware, especially on mobile devices, we'll rarely ever think about it. The language doesn't require us to.
The browser, too, is a famous abstraction. It provides a GUI that abstracts away, among many other things, the details of HTTP communication and HTML rendering. Users can easily browse the internet without ever having to worry about these mechanisms.
In the following chapters of this book, we'll learn more about what it takes to craft a good abstraction. For now, it's enough to say this: in every line of code you write, you are using, creating, and communicating abstractions.
The tower of abstraction is a way of looking at the complexity of technology. At the base layer, we have the hardware mechanisms depended upon in computation, such as transistors in the CPU and memory cells in RAM. Above that, we have integrated circuits. Above that, you have machine code, assembly, and the operating system. And above that, several layers up, you have the browser, and its JavaScript runtime. Each layer abstracts away complexity so that the layer above can leverage that complexity without too much effort:

When we write JavaScript for the browser, we are already operating on a very tall tower of abstraction. The higher this tower gets, the more precariously it operates. We are dependent on every individual part working as expected. It's a fragile system.
The tower of abstraction is a useful analogy when thinking about our users as well. When we write code, we are adding to this tower, building upon it layer by layer. Our users will always be situated above us on this tower, using the machinery we've crafted to drive their own ends. These users may be other programmers that utilize our code, building yet more layers of abstraction into the system. Alternatively, our users may be the end users of the software, typically sitting atop the tower and leveraging its vast complexity by a simplified GUI.
In the next part of the book, we will take the foundational concepts we've talked about in this chapter and build atop them with our own abstractions; these abstractions are the ones we, in the software industry, use to talk about what it means to write clean code.
If we say that our software is reliable or usable, then we are employing abstract concepts. And these concepts must be delved into. We will also be unpicking the innards of JavaScript in later chapters, seeing what it means to deal with the individual pieces of syntax that power our programs. By the end of the book, we should be able to say that we have complete knowledge of multiple layers of clean code, from individually readable lines of code to well-designed and reliable architectures.
In this chapter, we have built ourselves a great foundation, exploring the very fundamentals that underpin all of the code we write. We have discussed how our code is an expression of intent, and how, in order to build that intent, we must have a sound understanding of what the user requires and the problem domain we are engaging in. We have also explored how we can write code that is clear and readable to humans, and how we can create clear abstractions that provide users with the ability to leverage complexity.
In the next chapter, we will build on this foundation with the specific tenets of clean code: reliability, efficiency, maintainability, and usability. These tenets will be lending us a vital perspective as we go on to study the many facets of JavaScript and how we can wield it in the service of clean code.
In the last chapter, we discussed the purpose that sits at the very beginning of a piece of code: solving a problem for a user. We discussed the difficulties of catering to both the machine and the human. We reminded ourselves that, at its core, writing code is about communicating intent.
In this chapter, we will derive four core tenets from those foundations that are necessary to think about when creating software. These tenets are reliability, efficiency, maintainability, and usability. A good piece of software can be said to have all of these qualities. A bad piece of software can be said to have none of them. Crucially, however, these tenets are not rules. Instead, it is useful to see them as lenses through which you can look at your code. For each tenet, we will discover why it is important through a mix of analogies and JavaScript examples. You should come away from this chapter with an ability to apply these tenets to your code.
Specifically, we will be covering the following tenets:
Reliability is the core underpinning of a good software system. Without reliability, the usefulness of technology quickly dissipates, leaving us in a position where we may have been better going without it. Technology's entire purpose can be undermined by unreliability.
Reliability, however, is not only a characteristic of large and complex software systems. Each and every line of code can be constructed to be either unreliable or reliable. But what do we mean by this? Reliability, the word, refers to the quality of being reliable. What does it mean to write code that people can rely on? It helps by defining reliability in terms of three distinct qualities: Reliability is the quality of being correct, stable, and resilient.
Code that is correct is code which conforms to a set of expectations and requirements. If I write a function to validate email addresses, then the expectation is that the function can be called with all types of email addresses and correctly establish their validity or invalidity as follows:
isValidEmail('someone@example.org'); // => true
isValidEmail('foo+bar_baz@example.org'); // => true
isValidEmail('john@thecompany.$$$'); // => false
isValidEmail('this is not an email'); // => false
To write correct code, we must first have an idea of what the requirements are. Requirements are our formalized expectations for how the code will behave. For the previous case of an email validation function, we might have the following requirements:
The first requirement is ambiguous though. We need to discern what it means for an email address to even be valid. Email addresses are a seemingly simple format; however, there are in fact many edge cases and oddly valid manifestations. For example, the following email addresses are technically valid according to the RFC 5322 specification:
To know whether our function should align fully with the RFC specification, we first need to understand its true use case. Is it a utility for email client software, or is it perhaps utilized in user registration for a social media website? In the latter case, we may want to establish the more exotic email addresses as invalid, similar to those listed previously. We may even want to ping the mail server at the domain to confirm its existence. The point is to discern what exact requirements will provide for us the meaning of correct.
The requirements we are coding should always derive directly from how our code will be used. It is vital to start with the user and their problem; from that, we can establish a set of clear requirements that can be independently tested. Testing our code is necessary so that we can confirm, to ourselves and our stakeholders, that our code fulfills all of the distinct requirements.
With the previous example of email address validation, a good set of tests would encompass many variations of email addresses, ensuring that all edge cases are fully accounted for. In Chapter 13, The Landscape of Testing, we discuss testing in far more detail. For now, however, it's sufficient to simply reflect on the importance of correctness and the ways in which we can establish and confirm it:
Stability is a characteristic that we desire in all technology. Without stability, things get precarious; we become unsure of whether things will break at any moment. Stability is best exemplified by a common piece of real-world technology. Compare these two bridges:

They both technically operate correctly as bridges. However, one has previously suffered damage and has been fixed with a simple plank of wood. Which bridge would you trust to safely convey a hundred people across? Probably the one on the right. It is firmly in place with guard rails, and, crucially, no gaps through which you can fall.
In code, we can say that stability is about the continued correct behavior given different valid inputs and situations. JavaScript in the browser is especially liable to failures in this way. It has to run in a multitude of conditions, on different hardware, operating systems, screen sizes, and often in browsers that have varying capabilities.
If we write code that is strongly dependent on certain conditions, then it may be unwieldy and undependable when those conditions do not exist. If, for example, we were designing and implementing a layout for a web application, we may forget to consider and cater for screen sizes less than 1,024 pixels wide, resulting in the following mess:

This is an example of instability. The web application cannot be depended upon to deliver its correct behavior when a certain environmental factor is different. A situation in which screen size is less than 1,024 pixels is entirely possible and reasonable in a world of increasing mobile device usage; it's an absolutely valid use case of our web application and failure to accommodate it has a negative effect on our user's ability to rely on it.
Stability is gained through having a full understanding of all the different valid inputs and situations that your code may be exposed to. Similar to correctness, stability is best confirmed with a set of tests that expose your code to the full gamut of inputs and situations.
Resilience is about avoiding failure. Where stability mostly concerns itself with expected inputs, resilience concerns itself with what happens when your code is exposed to unexpected or nonroutine inputs. Resilience in software systems is also known as fault tolerance and is sometimes spoken about in terms of redundancies or contingencies. Fundamentally, these are all in service of the same goal—to minimize the effects of failure.
For critical systems, where lives depend on ongoing functionality, various contingencies are often built into the system. If a failure or fault arises, the system can isolate and tolerate that failure by utilizing its contingencies.
NASA, when building flight control systems for the Space Shuttle, built resilience into the system by having a set of synchronized redundant machines. If one failed, due to an unforeseen circumstance or a bug, then another would take over. Back on earth, we build contingency into our hospitals, with backup power generators that'll activate immediately if the electricity grid is down. Similarly, some urban transport networks benefit from contingencies in the form of replacement bus services in the case of trains not operating.
These large and complex systems may seem light years away from the world of JavaScript. But often, without realizing it, we are also routinely thinking about and implementing resilience into our code bases. One way we do this is via graceful degradation. When we write JavaScript for a browser environment, we have some key expectations:
If any of these conditions do not hold up, then the user might be faced with an entirely unusable website or web application. The way to alleviate these concerns is to build with graceful degradation in mind. Graceful degradation involves aspects of your application degrading to a state in which they can still be used, remaining useful to the user even in the face of unexpected failures.
Graceful degradation is often illustrated with a simple escalator:

An escalator, when functioning correctly, will convey people via a set of moving metallic steps driven by a powerful gear system and motor. If the system fails for whatever reason, then the escalator remains static, acting as a regular flight of stairs. So, the escalators can be said to be resilient because, even when unexpected failures occur, they remain usable. Users can still convey themselves up and down the escalators, though, perhaps the journey will take longer.
When writing JavaScript, we can build resilience into our code by detecting features we are relying upon and only employing them if they are available. For example, I might wish to play MP3 audio to a user. To accomplish this, I will make use of the HTML5 Audio element. Before doing this, however, I will detect whether the browser supports MP3 audio. If it doesn't, I can notify the user and point them toward a transcript of the audio instead:
function detectAudioMP3Support() {
const audio = document.createElement('audio');
const canPlayMP3 = audio.canPlayType &&
audio.canPlayType('audio/mpeg; codecs="mp3"')
return canPlayMP3 === 'probably';
}
function playAudio() {
if (detectAudioMP3Support()) {
// Code to play the audio
// ...
} else {
// Code to display link to transcript
// ...
}
}
The preceding code uses the HTMLMediaElement's canPlayType method to discern support. We've abstracted this into a detectAudioMP3Support function, which we then call to decide whether we'll go ahead and play the audio, or alternatively, display the transcript of the audio. Displaying the transcript of the audio is a form of graceful degradation as it still allows users to gain some utility from the audio without being able to play it.
It's important to note that feature detection by itself is not graceful degradation. If I detected MP3 support but then silently failed if it wasn't available, then that would not achieve much. However, the activation of an alternative pathway for our users—in this case, enabling the reading of the transcript—is a perfect example of graceful degradation and resilience to failure.
Resilience is a vital part of writing clean, reliable code. Fundamentally, we write code to solve problems for users. If our code can tolerate and accommodate edge-cases, unforeseen circumstances, and unexpected inputs, then it will fulfill this purpose more effectively.
We live in a world of scarcity. Resources are finite. In order to write the best possible code, we need to take this scarcity into account. So, when designing and implementing our ideas, we should be doing so with an eye toward efficiency.
In this section, we'll explore the different facets of efficiency and tie them into the world of JavaScript through examples. Hopefully, you will come away with an appreciation for how efficiency is not only about going fast but that it encompasses many indirect effects, spanning the gamut from economy to ecology.
One key scarcity that we are always aware of is time. Time is a vital resource that we should seek to only spend with due consideration. In the world of programming, we should seek to optimize the amount of time, or CPU cycles, spent on any given task. This is to be accommodating to our end users, as they themselves have limited time, but also to be prudent with limited and expensive hardware.
Given almost any function in JavaScript, there are ways for it to be written more efficiently and less efficiently. Take, for example, this function, which removes duplicate strings within an array:
function removeDuplicateStrings(array) {
const outputArray = [];
array.forEach((arrayItem, index) => {
// Check if the same item exists ahead of where we are.
// If it does, then don't add it to the output.
if (array.indexOf(arrayItem, index + 1) === -1) {
outputArray.push(arrayItem);
}
});
return outputArray;
}
This code is reliable, fulfills the requirements, and, for most intents and purposes, is perfectly fine. But it is doing needless work. On every iteration of the array, it is reiterating the entire array, from the current index to discover whether duplicate values exist ahead of where it is currently. This may seem a somewhat intuitive approach, but it is wasteful.
Instead of checking ahead of the entire input array, we can instead just check the existing output array for the specific value. If the output array already contains the value, then we know we don't need to add it again. Here is our slightly optimized if condition:
// Check if the same item exists already in the output array.
// If it doesn't, then we can add it:
if (outputArray.indexOf(arrayItem) === -1) {
outputArray.push(arrayItem);
}
There are other ways to optimize this, depending on how many unique values there are, and how large the input array is. We could, for example, store found values as keys in an object (a HashMap approach), which would, in some scenarios, decrease the lookup time.
Spending too long on a task can end up having a significant effect on the user's ability to perform their tasks. Later in the chapter, we will discuss the tenet of usability, but, for now, it's just important to note that efficiency with time is not only important on principle; it's important because, at scale, these often tiny efforts of efficiency can have massively positive effects on usability.
Space is a type of scarcity and is concerned with the size of things. Data shuttles across networks and into machines where it is stored ephemerally in RAM and possibly saved to permanent storage in the form of hard or solid-state-drives (HDDs, SSDs). As proponents of efficiency, we are interested in only using the space necessary to do a given task, and part of this is using the available space in the most efficient manner, and only moving data when it is prudent to do so.
Due to the high-level nature of the JavaScript language and the applications it usually builds, we rarely have to think about ephemeral RAM usage or permanent storage. However, JavaScript has gained significant ground as a legitimate language in performance-sensitive environments, such as database infrastructure and HTTP middleware, so these concerns are far more relevant nowadays.
Furthermore, the demands of client-side applications, both in the browser and within native environments, has drastically increased. The complexity of these applications means that we must always be on our toes, thinking about how to optimize memory and bandwidth usage on servers, user devices, and across increasingly layered networks. The bandwidth we absorb in a web application will have a direct effect on the time the user has to wait for the application to become usable.
Time and space efficiency are inextricably linked with both directly affecting the other. The overarching theme of efficiency is about only doing what's necessary, avoiding waste, and being thrifty with the resources you have available.
Efficiency in terms of space and time is responsible for many other effects, both in the software itself and in the wider world. No optimization exists in isolation. The savings made in one area will always have knock-on effects in other areas. And likewise, any needless costs will often create bottlenecks and issues further down the line.
There are too many of these effects to list, but some of the most obvious ones in the world of software would include the following:
It's essential to always consider the knock-on effects of the choices we make, whether those are made in the service of efficiency or some other requirement. None of what we create exists in a vacuum.
Maintainability is the ease with which appropriate changes can be made to your code. Unlike a motor vehicle, code does not typically need routine maintenance to avoid things such as rust, but it does, nonetheless, need to be fixed from time to time. Changes to its functionality are also often required, especially when under active development. Much of the code we work on is also being actively worked on by others. This shared ownership relies heavily on the tenet of maintainability.
Making code maintainable should not be a sidelined priority. It is as vital as any other requirement that the code is fulfilling. In the first chapter, we spoke a lot about the importance of considering who your users are. It would be disingenuous not to see that those who maintain and make changes to our code are also our users. They wish to wield what we have created to fulfill a purpose; therefore, they are our users. And, as such, we need to think about how we can best cater to their needs.
In this next section, we'll explore two aspects of maintainability: adaptability and familiarity.
Arguably, the best type of maintenance is that which does not need to occur. Adaptability refers to the ability of your code to cater to and adapt to different needs and environments. The code cannot be infinitely adaptive. The very nature of code is that it is made for a specific purpose; to solve a specific problem for the user. We can and should provide a level of configuration in our code, allowing for varying needs, but we cannot foresee all possibilities. Eventually, someone with new requirements may need to come along and make changes to the underlying code.
If we were to create a JavaScript component that displays a carousel of images (a slideshow), it's obvious to imagine that users will want to configure the specific images displayed. We may, for example, also have a configuration option for enabling or disabling the fade-in or fade-out behavior of the carousel. Our complete set of configuration options may look like this:
These configuration options define the extent to which our component is adaptable. It can be used in a variety of different ways by wielding these options. If a user wants it to behave in a way that is not made possible by these options, then they may wish to change its behavior by modifying the underlying code.
When changes need to be made to the underlying code, it is important that they can be made with as little trouble as possible. Two harmful characteristics that might cause trouble are fragility and rigidity:
Fragility and rigidity are usually symptoms of larger code bases, where there are many interdependencies between modules. This is why we say that modularity is so important. Modularity refers to the separation of concerns into distinct areas of code in a way that reduces intertwining code paths.
Working to avoid fragility and rigidity is a good goal and will make our code more accommodating to changes, but the most crucial aspect of a code base for a maintainer is its comprehensibility. That is, the extent to which it can be understood. Without understanding what, the maintainer cannot even begin to make changes. In fact, in obscure and confusing code bases, it is sometimes impossible to discern whether changes are even required. This is why we will now be exploring familiarity as an aspect of maintainability. By utilising familiar conventions and intuitive patterns, we can help to ensure a high level of understanding among our maintainers.
Familiarity is a lovely feeling. It's a feeling of comfort where you know what's happening because you've seen it before. This is the feeling we should hope to instill in all maintainers that may come across our code.
Imagine for a moment that you are a skilled mechanic. You open the hood of an old car. You expect all of the various components to be visible in their respective places. You are adept at recognizing specific components, and, even without having to move things about, you can see how components are linked together.
Some minor modifications have been made; maybe the owner has previously installed a turbocharged engine or modified the gear ratios, but overall, you see that everything is more or less where it should be. For you, the mechanic, making changes is going to be nice and simple:

In this example, everything under the hood is in its expected and designated location. Even though cars vary in many ways, their underlying functionality is the same, and so it makes sense that the layout and design are familiar to the mechanic.
When we think about it, software is not so dissimilar. Most software we end up creating is similar to other software in many ways. Most web applications, for example, will have a way for users to register, to login, and change their name. Most software, regardless of the problem domain, will have the concept of creation, read, update, and delete (CRUD). These make up the famous verbs of persistent storage. Most software can be thought of as fancy middleware sitting atop persistent storage. So, even though we may consider all software applications to be quite different, their fundamentals are usually very similar. It should, therefore, not be so difficult for us to write code that caters to the mechanic who opens the hood.
To make the mechanic's work as simple as possible, we need to focus foremost on the familiarity of our code. It's not simple to do this because different things are familiar to different people, but, on the whole, we can take heed from the following guides:
The last point alludes to unfamiliar problem domains. This is something that you, as the programmer, will have to think about when it comes to each code base you work on. To discern if something can be considered unfamiliar, you can ask yourself: Would another programmer working within another industry be able to understand this with little introduction?
While maintainability is predominantly about catering to other programmers, usability is about catering to all users, whoever they may be. We can say that there are two broad groups of users that are engaged in our service:
Usability is about making our code, and the functions and interactions it enables, as useful and easy to use as possible for the full gamut of users. All code is written with at least one use case in mind, and so it is fair to judge code based on the extent to which it fulfills that purpose. Usability, however, goes further than this. Usability is not only about fulfillment of user requirements; it's about creating experiences that enable a user to achieve their goals with minimal hassle, time, and cognitive effort.
Usability is vital whether we are creating user interfaces on the web or deeply embedded server infrastructure that'll rarely see the light of day. In both cases, there are users we are providing for, and so we must care about usability.
Have a look at this function's signature and try to discern how you would go about using it:
function checkIsNewYear(
configuration,
filter,
formatter,
MDY,
SMH
) {...}
This function is a real function signature from a code base I once worked on. It had no documentation and the code within it was spaghetti. It was used to calculate whether a given time could be considered a new year and would decide when a Happy New Year message would be shown to a user. It's incredibly unclear how to use it though, or how it works. Some open questions I would have upon discovering this function might be as follows:
The function may do as required, but, as you can see, it's not very usable. It takes significant time and cognitive effort to figure out how it works. To figure it out fully, we'd have to study its usages in other parts of the code and try to decipher the spaghetti within. As a user of this function, I would personally find the entire process utterly painful.
Usability is, if anything, about avoiding this pain and burden. As programmers, we engage in the creation of abstractions to simplify complex tasks, but all the preceding code achieves is further complication of a simple problem.
Usability is the degree to which something is easy to use for a given purpose. Its purpose is defined by a well-understood model of a problem and a set of clear requirements. A useful technique for articulating these purposes is via user stories, made famous by Scrum and Agile methodologies. User stories typically take the following form:
Here are some examples of the types of user stories you'd expect if we were designing a Contacts application:
User stories help to define the purposes that you are catering towards and help to focus the mind on the perspective of the user. Whether you're creating a five-line function or a ten-thousand-line system, it's always worth planning out your user stories.
To design something intuitively is to design it so that users don't have to dedicate cognitive effort to figure out how something works. The idea at the core of intuitive design is that it just works.
When we write code, we are partaking in its design, its grand architecture, its functionality, and its line-by-line syntax. All of these are vital parts of that design. Using intuitive patterns of design is vital to crafting usable code. All users are attuned to a set of patterns that are employed on their level of abstraction. Here are some examples:
These are the assumptions and expectations that many users carry around with them as they navigate software. Tapping into these assumptions means that your code and the interactions it facilitates can be monumentally easier to use.
Accessibility is a key principle within usability that states the importance of catering to all users, regardless of their abilities and circumstances. Usability tends to concern itself with the user as if they are a single entity. We usually make specific assumptions about the user, bestowing them with a set of characteristics and capabilities that may not be reflected in reality. Accessibility, however, is about the real users who will end up having to use whatever you've created. These real users are a diverse set of individuals and may have all manner of differences. When we talk about accessibility in software, we are usually concerned with types of differences that directly affect a person's ability to make use of that software. These may include the follows:
In addition to these, there are many other differences that span the gamut of human existence, so we should always be ready to learn and adapt according to new needs and differences that we encounter among our users.
We are engaged in the creation of web applications, both on the server and in the browser. As JavaScript programmers, we sit very close to the interfaces that are served to end users. Therefore, it's vital that we have an excellent grasp of accessibility on the web. This includes an awareness of the Web Content Accessibility Guidelines (WCAG 2.0) published by the W3C, which include the following provisions:
Accessibility is not only about the non-programmer end user. As mentioned, we should consider other programmers to also be our users in the same sense as the end users of GUIs or other web interfaces. It is vital that we cater to other programmers. Some programmers are blind or partially sighted. Some programmers have learning or cognitive difficulties. Not all programmers work on the latest and fastest hardware. Neither do all programmers understand all the things you may take for granted. It's important to take all of these things into consideration in the code we write.
Having now finished this chapter, you may have a sense of feeling overwhelmed by the number of tenets, principles, and guidelines. Things may seem complex, but they aren't if we follow one simple rule—always focus on the user. Also, remember that other programmers who may work on your code are your users as well.
As programmers, we sit in a position where we wield unprecedented power in helping to define the behaviors that people conduct in the execution of all manner of tasks. The original programmers who worked at Twitter, Google, or Microsoft likely did not foresee the number of times their code would run. They probably couldn't have originally imagined how many humans their code would end up affecting. We should always remain humble to this power and try in earnest to be accountable to all of the users we serve, and all the myriad tasks they seek to carry out. If you were to come away with one thing from this chapter, I hope it would be simply this: a humble and continued consideration of the user, in every line of code you write.
In this chapter, we've explored the vital tenets of reliability, efficiency, maintainability, and usability. Using these tenets as lenses through which we can look at our code bases ensures that we will be far more likely to write cleaner code. One of the most important things we've learned in this chapter is always to consider the human in the code we write. The user may be a human sitting on the other side of a GUI or a fellow programmer making use of our APIs. Either way, being constantly aware of this human is vital.
In the next chapter, we'll continue the theme of studying the underlying characteristics of clean code by looking at the enemies to be aware of, such as cargo-cult programming and ego.
By now, we should have a pretty clear picture of what we mean when we say clean code. In the previous chapter, we explored the tenets of reliability, efficiency, maintainability, and usability. Together, these guide us toward cleaner code, but if we aren't careful, we can still get caught out. In this chapter, we'll explore the enemies of clean code: things that may prevent us from writing code that is reliable, efficient, maintainable, or usable.
None of these enemies should be considered your enemies; instead, they should be thought of as agitators of clean code. We need to take a holistic view of these potentially harmful factors and keep an eye out for them in our code bases, teams, and workplaces.
Specifically, the enemies we'll be covering in this chapter include the following:
The worst JavaScript feature is also arguably its best. It is a remarkably ubiquitous language that has had to grow and adapt at a very fast rate. The language itself and its position within browser have precipitated this ubiquity.
JavaScript is an incredibly expressive and diverse language, with functional inspiration from Lisp and Scheme, prototypical inheritance from Self, and a C-like syntax that mirrors Java. It is a language of many paradigms. Whether you want to program in a classically object-oriented way, a prototypical way, or an entirely functional way, JavaScript has you covered. JavaScript's flexibility and its position in the broader web stack also make it incredibly accommodating to beginners. You can be immediately productive with it, and this was very much Brendan Eich's original intention. It was intended to be easy to pick up by designers and programmers alike, providing them with the power to script what was once a single-purpose platform: the browser. The once humble browser, however, has since grown into an incredibly broad and complex set of complementary abstractions.
The growth of JavaScript itself and its broad set of applications on both the client and server-side (and beyond!) has meant that the language has been pushed and pulled in a thousand different directions. An overwhelming number of frameworks, libraries, spin-off languages (for example, CoffeeScript), language extensions (for example, JSX), compilers, build tools, and other abstractions have erupted and have sought to leverage JavaScript in new and unique ways. Together, these tools form the JavaScript landscape, and it is an incredibly rich and diverse one. There are countless ways to do the same thing and, as a result, we can barely hope to do anything correctly. That is why I say that JavaScript's ubiquity is both its own worst enemy and its own greatest asset.
In this book, we'll explore foundational concepts that'll teach us to think critically about the nature of clean code and will allow us to write clean code within a language and landscape that don't always cater well code cleanness. JavaScript, if wielded well, will surprise you with its efficacy and expressiveness, and given time and effort, it can be the equal of any other language in terms of its reliability and maintainability.
Clean code is as much about the processes and principles that foster it as it is about syntax. No matter how perfect and beautiful our code is in isolation, it is usually written as part of a project, alongside a team, and managed by fallible people and fallible processes. And only by seeing and understanding these fallibilities can we hope to prevent or avoid them.
We are all taking on more challenging work nowadays. The days of JavaScript being limited to humble brochure websites with snazzy navigation rollovers are long gone. The creators of the web have been tasked with building ever more ambitious projects. As the technological tower of abstraction grows to new heights, the complexity of these projects will only increase. Due to this, if we are to truly write clean code, we must think broadly about this complexity. We must go beyond our code bases and consider the context of the team and the organization that we work in.
Casting management as an enemy may appear to suggest that managers themselves are blameworthy, but this is not the case. What we'll discover in this section is that it is individual cultural practices that make it challenging to ship clean code. Among these is the pressure to ship, bad metrics, and a lack of ownership.
The pressure to ship code, usually because of a deadline or other managerial dictates, is an ever-present and nasty force in the world of software. To the outside stakeholder or manager, a deadline is a great thing; it seems to provide certainty and accountability, but to the people working on the project, it might only be seen as enforcing unwelcome compromises. Sometimes, the first compromise that's made is that of code quality. This does not happen intentionally but is just a natural result of prioritizing completion over quality.
When there are pressures to ship, there are a few ways in which code quality can slowly atrophy. These include the following:
The preceding items are the first to go when deadlines begin to loom. If we're not careful, we can end up with the following second-order effects:
All of these effects, when they last long enough, can coalesce and result in the failure of a project. Therefore, it is of vital importance to address the underlying pressure that is forcing such recklessly high speed. The pressure to ship code quickly is a pressure that's usually instigated by forces that do not have a strong working knowledge of the slow degeneration that can occur in software projects. This lack of knowledge may, in part, be due to them being insulated from the long-term effects of their decisions. They may assume that, when something is shipped and meets the stakeholders' approval, that's the end of it. But as we know, just because quickly shipped code meets immediate demands doesn't mean it abides by good levels of quality. Bad-quality code can have many adverse knock-on effects that are only realized in full many weeks or months after implementation. Months later, stakeholders may find themselves annoyed at the slowdown and degradation in quality, not realizing that it was the pressure they originally applied that led to it.
The solution to this mess lies in a crucial compromise between time to ship and technical debt. Technical debt accrues over time. It describes deficits that need to be addressed to keep the code base healthy and in good working order. This may include fixing bugs, writing tests, refactoring old modules, or integrating tools to improve code quality. Fundamentally, technical debt is all work that, ideally, would be part of the natural development cycle but, because of time constraints, it is pushed aside until later. There are other factors that dictate the proliferation of technical debt, but time is the biggest one. Not paying off our technical debt is a sure way to ensure code atrophy and eventual failure of the project.
There are countless pieces of advice and processes that you can utilize when it comes to project management. I won't be going into them here, but I will share some heuristics that you can use to ensure a healthy code base:
We, as developers, are not always in control of the way that projects are managed. Nonetheless, we should always feel comfortable broaching concerns and advocating for processes that foster clean code. Chapter 18, Communication and Advocacy, goes into more detail on how we can go about this.
There are seemingly no industries in the world that can escape metrics. The crazed obsession with measuring things is as much a cult-like obsession as it is a genuine need that produces necessary introspection and change. In the world of software engineering, we are not strangers to this need. As programmers, we are very interested in metrics that provide us with insights into our code:
Managers and other stakeholders, however, will usually harbor their own interests and metrics. The more infamous among these are the metrics that attempt to measure a developer's output or productivity:
These are good questions if they're asked for the right reasons. For example, lines of code can be a useful metric if we're using them as a proxy for complexity when discussing whether to refactor specific classes/utilities. But many metrics are entirely divorced from the thing they are attempting to measure.
A non-technical manager or stakeholder may assume that writing a certain amount of code should always take the same amount of time. They may be confused when a developer who once wrote 200 lines of code in a single day has recently taken 10 days to commit only 10 lines of code. Their confusion, of course, demonstrates a gross misunderstanding of the programming process and its chaotic complexity. But these misunderstandings are rife, so we need to be wary of them.
The clear solution to bad metrics is to push for and create better metrics. And to create good metrics, it is essential to know what underlying question we're trying to answer and then brainstorm ways of answering that question. Let's take a look at an example:
| The question | The bad metric | Example of why it's bad | A better metric or approach |
| Are we being productive? | Lines of code/commits | A programmer could reasonably take many days to solve a crucial bug that only requires a one-line change. | Ask developers and explore what is dragging their productivity down; have team retrospectives to discover areas of improvement. |
| Are we delivering value to users? | Number of features shipped | Users may receive more benefit from fewer features that are of high quality. | Build metrics or A/B experiments to judge which features are being used and enjoyed. Focus on the quality of each feature. |
| Are we writing useful documentation? | Lines of documentation | Developers may only end up documenting the things they know well, not the areas of the code base that are most in need of documentation. | Create a metric that tracks the usage of documentation. Discern what areas of code are under-documented by asking developers. |
| Do we have a well-tested code base? | Test coverage | If it only measures whether certain lines of code are called, then it could be fooled with only a handful of very broad integration tests. | Use traditional test coverage in combination with other metrics. Keep track of areas of regression where bugs often occur. |
| Do we have a buggy code base? | Number of bugs | A code base may have many bugs in an area of the app that is virtually unused. Bugs in certain areas may be unreported. | Don't count bugs; instead, focus on and measure user happiness and developer happiness. Prioritize bugs based on how they are affecting your users. |
Fixation on bad metrics within an organization or team can lead to the wrong things being optimized. Developers who are more concerned with writing more lines of code will be less interested in the underlying quality of their code. Developers who are pushed to release more features will compromise on best practices and clean code, optimizing for speed and shipment.
It's important to ensure that any metrics we track are tempered by reality and that we do not judge success based purely on those metrics. Be especially wary when you see metrics running in opposition to our principles of clean code. Over time, as well, if a metric is chased too ambitiously, it may end up corrupting the very thing it was trying to measure. This is done via an effect known as Goodhart's law:
Ownership is a key tenet of a healthy code base and relies on individuals having a stake in the health of their code. Ownership here doesn't mean that a piece of code belongs to an individual and nobody else can work on it. Instead, it means that a piece of code is fostered by an individual or a group of people, with its ongoing health and reliability a key priority.
A lack of ownership can lead to the key tenets of clean code suffering in the following ways:
Properly applied ownership can fundamentally change the otherwise burgeoning atrophy of the preceding tenets:
Fundamentally, ownership is about an individual or a team that has a level of ongoing concern for the code. For this to occur, a level of ego or pride is necessary. An individual or team must have some kind of stake in the ongoing health of the code. It is often the organizational or managerial culture that leads to a healthy or unhealthy level of ownership, and so, again, it is vital to properly communicate and advocate processes and dynamics that will allow us, the programmers, to ensure our code's cleanliness and health.
There are also more severe and unimagined results of a lack of ownership. Due to the lack of pride and feeling of guardianship over our work, burnout becomes more likely as programmers aren't able to actualize their need to feel a sense of pride and self-worth regarding their work. Due to no ownership, team members may not be able to foster a high level of understanding in any one area, meaning that the general knowledge of the team or organization suffers, with everyone only understanding the code base in a very shallow or cursory way.
Programmers, as creators, are forever impressing their version of how something should be upon the world, so it's almost impossible to not, at times, feel a sense of pride over our work. If not kept in check, this can easily spiral into a place where we are writing code to impress people, and to boost feelings of our own superiority, without considering whether the code we're writing is maintainable or usable. But if our natural ego is not allowed to flourish, then we will have no pride in our work and no inclination to foster excellence in what we do. As such, in programming, as in other areas of life, the key is a balance of ego where we retain its good parts without letting its bad parts affect things too much.
As a younger programmer, I found my ego getting the better of me quite often. I don't presume to say this is a general truth. This is only my experience. Whenever I discovered a new JavaScript idiosyncrasy, I would try to exploit and make use of it in my next piece of code.
One example of this is the use of bitwise operators for their flooring effects. Traditionally, to floor numbers—to round a number down to its nearest whole number—you'd use the native method provided by the language:
Math.floor(65.7); // => 65
However, at the time, I preferred using bitwise operators to achieve the same result:
~~65.7; // => 65
0|65.7; // => 65
What is happening here? Bitwise operators (including ~, &, |, and so on) are used to mutate bits on operands, but as a side-effect, they will first convert their operands into 32-bit integers. This means they'll throw away the decimal fraction. To harness this implicit conversion into an integer without changing the value of the integer, we can perform, for example, a double bitwise inversion using the double tilde (~~). This essentially inverts all the bits of the operand and then inverts them again. We could also perform a bitwise OR with a zero (0|...), which will always return the bits of the non-zero operand, thus producing the same effect by harnessing the side-effect (the integer conversion) without changing the underlying value.
Crucially, it's important to note that this side-effect does not functionally match the flooring behavior of Math.floor for negative numbers. Note how the following two expressions differ:
Math.floor(-25.6); // => 26
~~(-25.6); // => 25
It's easy to see what's alluring about these cryptic techniques. Their usage seems to suggest a high level of language understanding, and that very much appeals to the ego. It's similar to using needlessly long or complex words to convey simple ideas: fun to say but alienating to the listener.
Techniques like this usually result in code that is less maintainable. The maintainers of our code should not be expected to understand the inner workings of rarely used operators and should be able to trust that we would not be recklessly employing side-effects of language internals to achieve results that can clearly be achieved via more familiar and obvious approaches.
Complex or rare syntax is often a vehicle for egotistic code. Another example of this lies in the misuse of logical operators to specify control flow:
function showNotification(message) {
hasUserEnabledNotifications() && (
new Notification(message).show() &&
logNotificationShown(message)
);
}
The preceding code can be more conventionally, and clearly, expressed as an IF statement:
function showNotification(message) {
if (hasUserEnabledNotifications()) {
new Notification(message).show();
logNotificationShown(message);
}
}
This is far clearer, more familiar, and more readable to a larger group of people.
Some people argue that we should be able to freely use the entire language to its full capability, harnessing all its idiosyncrasies and side-effects to write code that is terser and more efficient. This is a fine attitude to take if our only goal is to write code that works. But writing clean code is about taking a considered approach, using techniques that allow us to provide more readability and avoiding techniques that do the opposite.
It also helps to remember that, fundamentally, code is about communicating intent. Communication is as much about the listener as it is about the speaker. Egotistic code tends to fall short in this way; it limits the familiarity of your code to an elite few who have been blessed with the same knowledge that you have. This is not ideal. We should always try to take into account the diverse knowledge and capabilities of the people who will have to read, use, and maintain our code. This concern should take precedence over our ego.
Code is rarely written in isolation; we often work with people to bring projects to life. Clean code, therefore, depends on both your approach and the approach of the entire team. A team that owns a code base continuously decides the tools, conventions, and abstractions that they'll use to achieve their goals. As such, members of the team must be able to communicate well and share perspectives, molding these perspectives into a clear outcome. Sometimes, compromise is necessary. And compromise can often hit the ego.
JavaScript and its tools are susceptible to strong opinions. Over time, we each gain experience in working with different approaches and, often through toil and pain, end up having a set of beliefs about which approaches we think are best. These beliefs may not always match those of our colleagues, though. When there is disagreement, the path to resolution is unclear. Without resolution, the team and the code base can splinter, causing more damage.
Imagine the following scenario between Adam and Susan:
There are likely many different ways this disagreement could be resolved. We could suggest, for example, that both individuals build their case and continue to debate the various merits of each testing framework. That may resolve the issue. But equally, it may not. The argument may persist, drawing a wedge between the individuals and leaving the code base in a state of flux without a firmly chosen testing framework. The paths to resolution are not always clear in cases like this, but what is clear is that resolution is less likely if uncompromising egos are involved. If both Adam and Susan can start to see each other's perspectives, broadening their view and un-entrenching themselves from their own opinions, then the path to resolution becomes much clearer.
Ego, as a delicate trait, is also responsible for our level of faith and belief in our own capabilities and opinions. It is no wonder that having a level of belief in ourselves is vital to the act of creation and problem-solving in programming. In the technology industry especially, imposter syndrome seems to be a widespread occurrence. Imposter syndrome is characterized by a feeling of being an imposter—that you are somehow not suitable or sufficiently capable for the role you have, while you feel as though others around you are far more capable.
It can be argued that its prevalence in the software industry is due to the inherent complexity and wealth of specialties. We can, at best, hope for a high level of proficiency in a relatively narrow area but will never be expertly knowledgeable in all areas. We are, as we move about in our day-to-day work, ever aware of all the things we don't know, and this can understandably create a level of anxiety and lack of confidence in our own humble abilities. Such a feeling can sometimes cause stress, alienation, and a lack of confidence in our own abilities.
This may yield the following negative outcomes:
The act of programming is an act of communicating our intent, that is, of impressing upon the world, maybe in a small way, the way we believe a thing should work. It is itself a bold action and a skill that we should not take for granted. If you are reading this and are concerned that you may lack specific traits or capabilities, I offer the following advice: nobody on the planet is fully capable. Everyone has their strengths and weaknesses. It is the diversity of everyone and their varying capabilities that will define the success of a project and code base. Even if you feel a sense of imposter syndrome, acknowledge that it is natural to feel this way and that, in spite of it, you offer more than you might think.
In the early 20th century, it was observed that some Melanesian cultures would carry out rituals that would emulate Western technologies and behaviors, such as building runways and control towers out of wood and clay. They were doing this in the hope that material wealth, such as food, would be delivered to them. These odd rituals arose because they had previously observed cargo being delivered via Western planes and falsely concluded that it was the runway itself that summoned the cargo.
Nowadays, within programming, we use the terms cargo cult or cargo culting to broadly describe copying patterns and behaviors without fully understanding their true purpose and functionality. When programmers search for a solution online and copy and paste the first piece of code they find without consideration as to its reliability or safety, they are partaking in act of cargo culting, seeking to accomplish some task by utilizing code that appears to be responsible for it in some other context.
Cargo culting typically entails the following process:
This act can occur both organizationally and technically. Programmers, sometimes tasked with tying together disparate technical dependencies that they have little expertise in, will often be left with no other option than to cargo cult. And organizations, often without time to consider all the fundamentals, will often end up cargo culting popular behaviors and processes from other organizations.
To illustrate the act of cargo culting, let's imagine that a programmer is tasked with adding a new HTTP GET route to their Node.js server. They need to add the /about_us route. They open up the routes.js file and, among its many lines, find the following code:
app.use('/admin', (req, res, next) => {
const admin = await attemptLoadAdminSection(req, res);
if (admin) {
next();
} else {
res.status(403).end('You are not authorized');
}
});
This code happens to be using a Node.js framework: Express. Unfortunately, however, the programmer is not well versed in the Express API. They see the preceding code and seek to emulate it for their own ends:
app.use('/about_us', (req, res, next) => {
attemptLoadAboutSection(req, res);
next();
});
Unfortunately, as you may be able to tell, this programmer has committed the act of cargo culting. They've copied code that's used to route traffic toward the admin section and have assumed that they should use similar code to route traffic toward the about page.
There are a couple of things they've missed in doing so:
If the programmer had taken the time to read the Express documentation, they would have discovered that the correct way is more akin to the following:
app.get('/about_us', (req, res) => {
loadAboutSection(res);
});
This is a very brief example. Often, the act of cargo culting is more complex. It may not involve the direct copying of code, but maybe only the subtle copying of patterns or syntax. We may shake our head at the preceding example, sure of the knowledge that we would never do such a thing, but we likely already do, in less obvious ways.
Programmers that are engaged in a project will often rightfully inherit the naming, syntax, and whitespace conventions from the existing code base. They may do this without thought, naturally reflecting and conforming to the existing paradigms without applying their critical skills at every step. This isn't necessarily negative: it is the sensible upholding of conventions and presentational consistency. These are important qualities. But equally, the mindless copying of such things can often result in the pointless proliferation of redundant code, or worse, negative effects due to misunderstood code.
Imagine you're a first-time programmer and you wanted to add a hobby field to the following slightly bizarre object:
const person = {
"name": ("James"),
"location": ("London")
};
It's easy to imagine that you might be inclined to copy the existing syntax when you add your new field:
const person = {
"name": ("James"),
"location": ("London"),
"hobby": ("kayaking")
};
This is an entirely reasonable thing for a first-timer to have done. They were embedded in an unfamiliar context, saw an effect they wished to emulate, and so they adopted the pattern that produced the effect. It is even an understandable act by someone experienced, who wants to make the minimal necessary changes to the code surgically and without disturbing its surroundings.
There is nothing egregiously wrong in this code. It's functional. However, if we are to write code that is maximally maintainable and efficient, then we should adopt conventions and syntax that are more widely accepted and conventional. So, in this light, there are two specific problems with the preceding code:
The non-cargo culted version of the file might look like this:
const person = {
name: "James",
location: "London",
hobby: "kayaking"
};
However, this file and object will likely live on for months and maybe years to come. Nobody will ever question or challenge its syntax as they'll assume it must be like that for a reason. There is comfort and ease in conforming to an established way of doing something. It is often easier not to challenge it. This form of cargo culting is the more insidious type and introduces a lot of inertia to projects and teams. We mindlessly adopt practices without questioning their continuing validity and suitability.
Just as code can be mindlessly copied, so can tools. As JavaScript programmers, we are exposed to a quickly shifting landscape of tools and libraries. Every month, a new utility or tool seems to be released. The excitement and hyperbole that surrounds some of these tools creates fertile ground for cargo cults to erupt. Programmers may start to use these new tools, convinced of their merit, without building a full understanding of them or properly considering their suitability for the project at hand. Tools may be prescribed by companies or managers, with non-programmers and programmers alike weighing in based purely on a tool's popularity or novelty, without considering how it actually works or how it differs from the current approach.
The cult in cargo cults tends to be a very persuasive force, telling us that if we just use this approach or tool, all our problems will be solved. Naturally, this rarely comes to pass. We may only end up exchanging our current set of problems for a new set of problems. So, when deciding upon a tool, whether it is a framework, library, or any third-party abstraction or service, we should always use a considered approach where we ask ourselves the following key questions:
To avoid cargo culting, we should try to abstain from anecdotes and hearsay, instead preferring detailed comparative analyses in which we compare and contrast various possibilities to find the most suitable.
In this chapter, we gained an appreciation of some of the most prevalent enemies of clean code. We discussed how JavaScript itself is a language that, when wielded incorrectly, can invite unclean code. We also explored the pitfalls of both teams and the individual. We learned that clean code is not merely a characteristic of code but a culture that must be fostered both throughout an organization and within our own minds.
In the next chapter, we will explore some well-known and some less well-known principles of clean code and integrate what we've learned so far into some concrete JavaScript abstractions.
The world of software is riddled with principles and acronyms. There are many firm and entrenched ideas about how we should go about writing code. The sheer quantity of all of them can be overwhelming, making it especially hard to know which path to take when designing an abstraction. JavaScript's ability to accommodate many different paradigms is one of its strengths as a programming language, but it can also make our job harder. It's up to JavaScript programmers to implement their own paradigms.
This chapter, in the hope of making things less complicated, will take various well-known principles and break them down so we can see their underlying intent. We will explore how these principles relate to the tenets of clean code that we have already discussed, enabling us to make our own informed decisions as to what approaches to use in pursuit of clean code.
We'll be covering both object-oriented and functional programming principles. By exploring this range of principles, we'll be able to craft, for ourselves, a map of guiding ideas that will enable us to think critically about how to write clean code in whatever paradigm we're engaged in.
In this chapter, we will be covering the following topics:
Before we delve into the SOLID arena, it's useful to explore a less well-known principle, known as LoD, or the principle of least knowledge. This so-called law has three core ideas:
You may rightfully wonder what it means for a unit to talk to a stranger. A unit, in this context, is a specific coded abstraction: possibly a function, a module, or a class. And talking here means interfacing with, such as calling the code of another module or having that other module call your code.
This is a very useful and simple law to learn and then apply to all our programming, whether we're writing an individual line of code or designing an entire architecture. It is, however, often forgotten or ignored.
Let's take the example of the simple act of making a purchase in a shop. We can express this interaction with Customer and Shopkeeper abstractions:
class Customer {}
class Shopkeeper {}
Let's also say that the Customer class has a wallet where they store their money:
class Customer {
constructor() {
this.wallet = new CustomerWallet();
}
}
class CustomerWallet {
constructor() {
this.amount = 0;
}
addMoney(deposit) {
this.amount += deposit;
}
takeMoney(debit) {
this.amount -= debit;
}
}
A simplified version of an interaction between the Shopkeeper and the Customer may go something like the following globally:
class Shopkeeper {
processPurchase(product, customer) {
const price = product.price();
customer.wallet.takeMoney(price);
// ...
}
}
This may look okay, but let's consider a real-life analogy of this interaction. The shopkeeper takes the wallet from the customer's pocket and then proceeds to open the wallet and take the desired amount without in any way interacting with the customer directly.
It's immediately obvious that this would never be a socially appropriate interaction in real life, of course, but crucially, the shopkeeper is making assumptions outside of their remit. The customer may wish to pay using a different mechanism, or may not even have a wallet. The nature of the customer's payment is their own business. This is what we mean when we say only talk to friends: you should only interface with abstractions that you should have knowledge of. The shopkeeper here should not (and would not) have knowledge of the customer's wallet and so should not be talking to it.
Taking this learnings on board, we can program a cleaner abstraction as follows:
class Shopkeeper {
processPurchase(product, customer) {
const price = product.price();
customer.requestPayment(price);
// ...
}
}
This now seems more reasonable. The Shopkeeper is talking to the Customer directly. The customer, in turn, will talk to their CustomerWallet instance, retrieving the desired amount and then handing it to the shopkeeper.
We have all likely written code that somewhat violates the LoD. Of course, the code we write is not always as contrived or neatly exemplified by real-life as the interaction between a shopkeeper and a customer, but nonetheless, the LoD still applies. We can illustrate this further with a typical piece of JavaScript that is responsible for displaying a message to the user via the document object model (DOM):
function displayHappyBirthday(name) {
const container = document.createElement('div');
container.className = 'message birthday-message';
container.appendChild(
document.createTextNode(`Happy Birthday ${name}!`)
);
document.body.appendChild(container);
}
This is quite typical and idiomatic frontend JavaScript. To display the Birthday message within a document, we first construct the string ourselves and place it in a text node, which itself is appended to a <div> element with message and birthday-message classes. We then take this DOM tree and append it to the document so it can be viewed by the user.
Does the preceding code abide by the LoD? Our abstraction here, the displayHappyBirthday function, is concerned with the concept of a happy birthday message and is talking directly to the DOM. The DOM, however, is not its friend. The DOM is an implementation detail—a stranger—in the concept of a Happy Birthday message. The Happy Birthday message mechanism should not be required to have knowledge about the DOM. It would, therefore, be appropriate to build another abstraction that bridges these two strangers:
function displayMessage(message, className) {
const container = document.createElement('div');
container.className = `message ${className}`;
container.appendChild(
document.createTextNode(message)
);
document.body.appendChild(container);
}
Here, we have a more generic displayMessage function that is interfacing directly with the DOM—a friend. Our displayHappyBirthday function could then be changed so that it purely interacts with this displayMessage abstraction:
function displayHappyBirthday(name) {
return displayMessage(
`Happy Birthday ${name}!`,
'birthday-message'
);
}
This code can now be said to be more loosely coupled to the implementation of displayMessage. We could later decide to change the exact mechanism that we use to display messages without altering the displayHappyBirthday function at all. We've therefore bolstered the maintainability of code. By generalizing a common piece of functionality—displaying a message—we also make future features much more seamless—for example, displaying a Happy New Year message:
function displayHappyNewYear(name) {
return displayMessage(
`Happy New Year! ${name}`,
'happy-new-year-message'
);
}
The LoD, at its core, is concerned with which abstractions we feel should interface with other abstractions. It does not provide guidance as to what a friend or a stranger is or what it means for a unit to only have limited knowledge of other units. The law challenges us to define these terms for ourselves, alongside the abstractions we're building. It's our responsibility to stop and consider how our abstractions are interfacing, and whether perhaps we should design them differently.
I chose to write about this principle first as I feel it is the most memorable and most generally useful tool for writing clean code with clean abstractions.
Next, we'll be discussing SOLID and other principles that all, in their own ways, complements the LoD.
SOLID is a commonly packaged set of principles that are useful when constructing both individual modules or larger architectures. Specifically, it is an acronym that stands for five specific object-oriented programming (OOP) design principles:
It is not vital to remember these names or even the acronym itself, but the ideas behind each of these principles are useful. In this section, we're going to explore each principle alongside JavaScript examples. It's important to note that, while SOLID relates mostly to OOP, there are deeper truths underlying it that are useful regardless of your programming paradigm.
When we write code, we are constantly building abstractions; when doing this, we are interested in building the right ones, delineated in the right way. The SRP helps us to figure out how to delineate these abstractions by looking at their responsibilities.
Responsibility, in this context, refers to the purpose and area of concern that your abstraction encompasses. A function that validates phone numbers can be said to have a singular responsibility. A function that both validates and normalizes those numbers with their country codes, however, can be said to have two responsibilities. The SRP would tell us that we need to split that abstraction into two separate ones.
The aims of the SRP are to arrive at code that is highly cohesive. Cohesiveness is when an abstraction's parts are all functionally united in some way, where they can all be said to work together to fulfill the abstraction's purpose. A useful question about discerning singular responsibility is: how many reasons does your abstraction's design have to change?
We can explore this question using an example. Say that we are tasked with building a small calendar application. We might imagine, initially, that there are two distinct abstractions here:
class Calendar {}
class Event {}
The Event class can be said to contain time and metainformation about an event, and the Calendar class can be said to contain events. The basic starting premise is that you can both add and remove one or more Event instances to and from a Calendar instance. Here, we express the methods used to add and remove events from Calendar:
class Calendar {
addEvent(event) {...}
removeEvent(event) {...}
}
Over time, we have to add various other pieces of functionality to our Calendar, such as methods for retrieving events within specific dates, and methods to export events in various formats:
class Calendar {
addEvent(event) {...}
removeEvent(event) {...}
getEventsBetween(stateDate, endDate) {...}
setTimeOfEvent(event, startTime, endTime) {...}
setTitleOfEvent(event, title) {...}
exportFilteredEventsToXML(filter) {...}
exportFilteredEventsToJSON(filter) {...}
}
Even without implementations, you can see how the addition of all of these methods has created a far more complex class. Technically, all of these methods are related to the functionality of a calendar, so there is an argument for them to remain within one abstraction, but if we go back to the question we posed—How many reasons does our abstraction's design have to change?—we can see that the Calendar class now has many possible reasons:
Given the number of different reasons for potential change, it makes sense to split the change into more appropriate abstractions. The methods for setting the time and title of a particular event (setTimeOfEvent, setTitleOfEvent), for example, definitely make sense within the Event class itself, as they're highly related to the purpose of the Event class: to contain details regarding a specific event. And the methods that export to both JSON and XML should also be moved, perhaps into their own class that is solely responsible for the export logic. The following code shows the changes that we've made:
class Event {
setTime(startTime, endTime) {...}
setTitle(title) {...}
}
class Calendar {
addEvent(event) {...}
removeEvent(event) {...}
getEventsBetween(stateDate, endDate) {...}
}
class CalendarExporter {
exportFilteredEventsToXML(filter) {...}
exportFilteredEventsToJSON(filter) {...}
}
As you can hopefully see, each of our abstractions seems inwardly cohesive, and each one encapsulates its responsibilities far more cohesively than would be the case if all of that functionality resided solely within the Calendar class.
The SRP is not only about creating abstractions that are simple to use and maintain, it also allows us to write code that is more focused on its key purpose. Being more focused in this way gives us a clearer path to optimize and test our units of code, which benefits the reliability and efficiency of our codebase. The correct delineation of cohesive abstractions, guided by the SRP, is probably one of the most significant ways you can improve the cleanness of your code.
The open–closed principle (OCP) states the following:
When crafting abstractions, we should enable them to be open to extension so that other developers can come along and build upon their behavior, adapting the abstraction to suit their needs. Extension, in this context, is best thought of as a broad term that encompasses all types of adaptation. If a module or function does not behave as we require it to, it would be ideal for us to be able to adapt it to our needs without having to modify it or create our own alternative.
Consider the following Event class and renderNotification method from our Calendar application:
class Event {
renderNotification() {
return `
You have an event occurring in
${this.calcMinutesUntil()} minutes!
`;
}
// ...
}
We may wish to have a separate type of event that renders a notification prefixed with the word Urgent! to ensure that the user pays more attention to it. The simplest way to achieve this adaptation is via inheritance of the Event class, as follows:
class ImportantEvent extends Event {
renderNotification() {
return `Urgent! ${super.renderNotification()}`;
}
}
We are prefixing our urgent message by overriding the renderNotification method and calling the super class's renderNotification to fill in the remainder of the notification string. Here, via inheritance, we have achieved extension, adapting the Event class to our needs.
Inheritance is only one way that extension can be achieved. There are various other approaches that we could take. One possibility is that, in the original implementation of Event, we foresee the need for custom notification strings and implement a way to configure a renderCustomNotifcation function:
class Event {
renderNotification() {
const defaultNotification = `
You have an event occurring in
${this.calcMinutesUntil()} minutes!
`;
return (
this.config.renderCustomNotification
? this.config.renderCustomNotification(defaultNotification)
: defaultNotification
);
}
// ...
}
This code presumes that there is a config object available. We are optionally calling the renderCustomNotification and passing the default notification string. If it hasn't been configured, then the default notification string is used anyway. This is crucially different from the inheritance approach in that the Event class itself is prescribing the itself is prescribing the possibilities possibilities extension that exist.
Providing adaptability via configuration means that users don't need to worry about the internal implementation knowledge required when extending classes. The path to adaptation is simplified:
new Event({
title: 'Doctor Appointment',
config: {
renderCustomNotification: defaultNotification => {
return `Urgent! ${defaultNotifcation}`;
}
}
});
This approach requires that your implementation can foresee its most likely adaptations and that those adaptations are predictably internalized into the abstraction itself. However, it is impossible to foresee all needs, and even if we tried to, we would likely end up creating such a complicated and large configuration that users and maintainers would suffer. So there is a balance to strike here: adaptability is a good thing, but we also must balance it with a focused and cohesive abstraction that has a constrained purpose.
The Liskov substitution principle states that types should be able to be replaced by their subtypes without altering the reliability of the program. This is, on the surface, an obscure principle, so it's worth explaining it in terms of a real-world analogy.
Many real-world technological innovations share this characteristic of substitution. A Volvo XC90 is a type of car, as is a Ford Focus. Both provide the common interfaces that we have come to expect from cars. For us, as human users of these vehicles, we can assume that their respective means of operation inherit from a common schema of vehicle operation, such as having a steering wheel, doors, a brake pedal, and so on.
The human assumption is that these two models of car are subtypes of the supertype car, and so I as a human can rely on the aspects that they each inherit from their supertype (the car). Another way of phrasing the Liskov substitution principle is: A consumer of a type should only be concerned with the least specific type necessary to operate it reliably. To build on the analogy, if we were to program a Driver abstraction in code, we would want it to interface generally with all Car abstractions rather than writing specific code that relies on specific models of car (such as the Volvo XC90).
To make the Liskov substitution principle a little more concrete, let's dive back into our Calendar application example. In the preceding section on the open-closed principle, we explored how to extend the Event class via inheritance with a new ImportantEvent class:
class ImportantEvent extends Event {
renderNotification() {
return `Urgent! ${super.renderNotification()}`;
}
}
The assumption implicit in our doing this is that our Calendar class and its implementation will not be concerned with whether events are Events or subclasses of Events. We expect that it will treat them the same. The Calendar class may have a notifyUpcomingEvents method, for example, that iterates through all upcoming events and calls renderNotification on each event:
class Calendar {
getEventsWithinMinutes(minutes) {
return this.events.filter(event => {
return event.startsWithinMinutes(minutes);
});
}
notifiyUpcomingEvents() {
this.getEventsWithinMinutes(10).forEach(event => {
this.sendNotification(
event.renderNotification()
);
});
}
// ...
}
What's crucial here is that the Calendar implementation makes no deliberations as to the type of Event instance that it is dealing with. In fact, the preceding code (which doesn't account for the entire implementation) only prescribes that event objects have a startsWithinMinutes method and a renderNotification method.
Related to the Liskov substitution principle is an idea that we've already discussed: the principle of least information (LoD), which drives us to ask: what is the least information that this abstraction requires in order to fulfill its purpose? In the case of Calendar, it only needs to prescribe that events have the methods and properties that it will use. There is no good reason for it to make deliberations beyond that. Not only can the Calendar implementation now deal with subclasses of events, but it can deal with any objects that supply the desired properties and methods.
The interface segregation principle is concerned with keeping interfaces highly cohesive, engaged in only one task or a set of tasks that are highly related. It states that no client should be forced to depend on methods that it does not use.
This principle is similar in spirit to the principle of single responsibility: its goal is to ensure that you create focused and highly-cohesive abstractions that are only concerned with a single area of responsibility. But instead of making you consider the concept of responsibility itself, it makes you look at the interfaces that you're creating and consider whether they're appropriately segregated.
Consider a local government office. They have a paper form (let's call it Form 1A) that it uses to change a person's details. It is a form that's existed for over 50 years. Via this form, a local citizen can change a number of their details, including, but not limited to, the following:
As you can imagine, it's a very complex and dense form, with many independent sections that a citizen must ensure they fill out correctly. We've all likely been exposed to the bureaucratic complexity of government paperwork, as shown in the following:

Form 1A provides a set of interfaces to the various change functions that are provided by the local government office. The interface segregation principle asks us to consider whether this form is forcing its clients to depend on methods that they don't use. In this context, the clients are the users of the form, the citizens, and the methods are all of the available functions that the form provides: the ability to register a name change, an address change, and so on.
As is hopefully obvious by now, Form 1A does not follow the interface segregation principle very well. If we were to redesign it, we would likely separate out the individual functions it serves into their own independent forms. The way we'd do this is by employing something we learned at the beginning of the chapter: the principle of least information (LoD), which asks us a very simple question: What's the least amount of information each abstraction (for example, changing one's address) requires? We can then choose to only include in each form what is needed to fulfill its function:

To separate out and then only include the necessary fields in paper forms such as these may seem quite obvious, but it's something programmers continually neglect to do effectively within their coded abstractions. The interface segregation principle reminds us of the importance of properly separating out our abstractions into interfaces that are distinct and internally cohesive. Doing so has the following benefits:
The dependency inversion principle states the following:
The first point may remind you of the LoD. It is largely talking about the same concept: the separation of high-level from low-level.
Our abstractions should be separated (decoupled) in such a way that we can easily change low-level implementation details at a later date without having to refactor all of our code. The dependency inversion principle, in its second point, suggests that we do this via intermediary abstractions through which the high-level modules can interface with the low-level details. These intermediary abstractions are sometimes known as adapters, as they adapt a low-level abstraction for consumption by a high-level abstraction.
Considering our Calendar application once again, let's say that we wanted to implement a way to retrieve events happening within a specific radius of a fixed location. We may choose to implement a method like so:
class Calendar {
getEventsAtLocation(targetLocation, kilometerRadius) {
const geocoder = new GeoCoder();
const distanceCalc = new DistanceCalculator();
return this.events.filter(event => {
const eventLocation = event.location.address
? geocoder.geocode(event.location.address)
: event.location.coords;
return distanceCalc.haversineFormulaDistance(
eventLocation,
targetLocation
) <= kilometerRadius / 1000;
});
}
// ...
}
The getEventsAtLocation method here is responsible for retrieving events that are within a certain radius (measured in kilometers) from a given location. As you can see, it uses both a GeoCoder class and a DistanceCalculator class to achieve its purpose.
The Calendar class is a high-level abstraction, concerned with the broad concepts of a calendar and its events. The getEventsAtLocation method, however, contains a lot of location-related details that are more of a low-level concern. The Calendar class here is concerns itself with which formula to utilize on the DistanceCalculator and the units of measurement used in the calculation. You can see how, for example, the kilometerRadius argument must be divided by 1000 to get the distance in meters, which is then compared to the distance returned from the haversineFormulaDistance method.
All of these details should not be the business of a high-level abstraction, such as Calendar. The dependency inversion principle asks us to consider how we can abstract away these concerns to an intermediary abstraction that acts as a bridge between high-level and low-level. One way in which we may accomplish this is via a new class, EventLocationCalculator:
const distanceCalculator = new DistanceCalculator();
const geocoder = new GeoCoder();
const METRES_IN_KM = 1000;
class EventLocationCalculator {
constructor(event) {
this.event = event;
}
getCoords() {
return this.event.location.address
? geocoder.geocode(this.event.location.address)
: this.event.location.coords
}
calculateDistanceInKilometers(targetLocation) {
return distanceCalculator.haversineFormulaDistance(
this.getCoords(),
targetLocation
) / METRES_IN_KM;
}
}
This class could then be utilized by the Event class in its own isEventWithinRadiusOf method. An example of this is shown in the following code:
class Event {
constructor() {
// ...
this.locationCalculator = new EventLocationCalculator();
}
isEventWithinRadiusOf(targetLocation, kilometerRadius) {
return locationCalculator.calculateDistanceInKilometers(
targetLocation
) <= kilometerRadius;
}
// ...
}
Therefore, all the Calendar class needs to concern itself with is the fact that Event instances have isEventWithinRadiusOf methods. It needs no information and makes no prescriptions as to the specific implementation that determines distances; the details of that are left to our lower-level EventLocationCalculator class:
class Calendar {
getEventsAtLocation(targetLocation, kilometerRadius) {
return this.events.filter(event => {
return event.isEventWithinRadiusOf(
targetLocation,
kilometerRadius
);
});
}
// ...
}
The dependency inversion principle is similar to other principles that are related to the delineation of abstractions, such as the interface segregation principle, but is specifically concerned with dependencies and how these dependencies are directed. As we design and build abstractions, we are, implicitly, setting up a dependency graph. For example, if we were to map out the dependencies for the implementation that we arrived at, then it would look something like this:

It's incredibly useful to draw dependency graphs such as these. They are a useful way to explore the true complexity of your code, and can often highlight areas of possible improvement. Most importantly, they let us observe where, if anywhere, our low-level implementations (details) impact our high-level abstractions. Only when we see such situations can we remedy them. So, as you advance through this book and beyond, always have in your mind's eye a graph of the dependencies; it'll help to steer you toward more decoupled, and thus more reliable, code.
The dependency inversion principle is the very last of the SOLID acronym, and SOLID, as we've seen, is chiefly concerned with how we go about building abstractions. The next principle we'll cover binds together a lot of the content we've covered so far, as it is the principle of abstraction itself. If we remember nothing else from this chapter, then we should, at least, remember the abstraction principle.
In the first chapter, we introduced the concept of a tower of abstractions, and the idea that every abstraction is a simplified lever to hidden complexity. The principle of abstraction within programming states the following:
An implementation is the complex underside of an abstraction: the part that it's hiding. The interface is the simplified topside. That is why we say that abstraction is a simplified lever to hidden complexity. The craft of creating abstractions that separate implementation from interface to just the right degree is not as simple as it may seem. As such, the programming world provides two warnings for us:
Between these two warnings, somewhere in the middle, lies the perfect abstraction. Designing abstractions so that they are maximally simple and maximally useful is a balancing act. On the one hand, we can say we have under-abstraction (DRY warns us about this) and, on the other hand, we have over-abstraction (YAGNI warns us about this).
Over-abstraction is when too much complexity has been removed or replaced, so that the underlying complexity becomes difficult to leverage. The risk with over-abstraction is that we either remove too much complexity in favor of simplicity or we add new unnecessary complexity that confuses the user of our abstraction.
For example, say that we are in need of a gallery abstraction that we want to use to display a gallery on both our website and various mobile applications. Depending on the platform, the gallery will use the interfaces available to produce the layout. On the web, it would produce HTML and DOM, but on a mobile application, it would use the various native UI SDKs available. The abstraction provides a lever to all that cross-platform complexity.
Our initial requirements for the gallery are quite simple:
An external team has created a Gallery component for us to use. We open the documentation and see that it has the following example code, showing us how to create a gallery with two images:
const gallery = new GalleryComponent(
[
new GalleryComponentImage(
new GalleryComponentImage.PathOfImage('JPEG', '/foo/images/Picture1.jpg'),
new GalleryComponentImage.Options({
imageDimensionWidth: { unit: 'px', amount: 200 },
imageDimensionHeight: { unit: 'px', amount: 150 },
customStyleStrings: ['border::yellow::1px']
}),
[
new GalleryComponentImage.SubBorderCaptionElementWithText({
content: { e: 'paragraph', t: 'The caption for this employee' }
})
]
}),
new GalleryComponentImage(
new GalleryComponentImage.PathOfImage('JPEG', '/foo/images/Picture2.jpg'),
new GalleryComponentImage.Options({
imageDimensionWidth: { unit: 'px', amount: 200 },
imageDimensionHeight: { unit: 'px', amount: 150 },
customStyleStrings: ['border::yellow::1px']
}),
[
new GalleryComponentImage.SubBorderCaptionElementWithText({
content: { e: 'paragraph', t: 'The caption for this employee' }
})
]
})
]
);
This interface seems very complex for the basic purpose of only displaying a couple of images. Considering our simple requirements, we can say that the preceding interface is evidence of over-abstraction: instead of simplifying the underlying complexity, it has introduced a whole new realm of complexity and various features that we don't even need. It does technically fulfill our requirements, but we must navigate its realm of complexity to achieve what we want.
An abstraction like this, which encodes new complexities and prescribes its own features and naming conventions, is at risk of not only failing to reduce complexity, but also of increasing it! An abstraction has no business in increasing complexity; that is antithetical to the entire point of abstraction.
Over-abstraction can, curiously, also take the form of over-simplification, where levers to the underlying complexity are not made available to us. An oversimplified version of our GalleryComponent interface may look like the following:
const gallery = new GalleryComponent(
'/foo/images/PictureOne.jpg',
'/foo/images/PictureTwo.jpg'
);
This minimal interface may seem like the polar opposite of the previous code, and in some ways it is, but curiously, it is also an example of over-abstraction. Remember, abstraction is when we provide a lever to underlying complexity via an interface. In this case, the lever is just too simple, only providing very limited leverage for the complexity that we wish to harness. It does not allow us to add captions or control image dimensions; it only allows us to list a set of images, nothing more.
Having gone through the two previous examples, you've seen how over-abstraction can come in two distinct flavors: one that over-complicates and one that over-simplifies. These are both undesirable.
If over-abstraction is when too much complexity has been removed or replaced, then under-abstraction is when too little complexity has been removed or replaced. This results in a situation where the user of the abstraction then needs to concern themselves with the underlying complexity. Imagine that you have a car that you must drive without a steering wheel or dashboard. You must control it directly via the engine by pulling levers and cranking oily cogs with your bare hands while keeping an eye on the road. We can say that this car has an under-abstracted method of control.
We explored the over-abstracted versions of our gallery component, so let's see what an under-abstracted version might look like:
const gallery = new GalleryComponent({
web: [
() => {
const el = document.createElement('div');
el.className = 'gallery-container';
return el;
},
{
data: [
`<img src="/foo/images/PictureOne.jpg" width=200 height=150 />
<span>The caption</span>`,
`<img src="/foo/images/PictureTwo.jpg" width=200 height=150 />
<span>The caption</span>`
]
}
],
android: [
(view, galleryPrepData) => {
view.setHasFixedSize(true);
view.setLayoutManager(new GridLayoutManager(getApplicationContext(),2));
return new MyAdapter(getApplicationContext(), galleryPrepData());
},
{
data: [
['/foo/images/PictureOne.jpg', 200, 150, 'The Caption']
['/foo/images/PictureTwo.jpg', 200, 150, 'The Caption']
]
}
]
});
This version of GalleryComponent seems to be forcing us to define web-specific HTML and Android-specific code. We were, ideally, depending on the abstraction to hide this complexity from us, giving us a simplified interface with which to harness—it hasn't done this. The complexity of writing platform-specific code has not been sufficiently abstracted here, and so we can therefore say that this is an example of under-abstraction.
From the previous code, you can also see that we are being made to repeat the source URL of our image and the caption text. This should remind us of one of the warnings we explored earlier: DRY, which indicates that we have not sufficiently abstracted something.
If we keep an eye out for areas in which we are forced to repeat ourselves, then we can hope to build better abstractions. But be aware that under-abstraction is not always obvious.
Various abstractions can be said to be leaky abstractions because they leak parts of their complexity upwards, through their interfaces. The previous code is an example of this: we can say that it is leaking the implementation details of its cross-platform complexities upward.
Given what we've learned about under- and over-abstraction, we can say that a balanced abstraction is one that sits neatly in between these two undesirable opposites. The skill of creating a balanced abstraction is both an art and a science, and requires that we have a very good understanding of both the problem domain and the user's capabilities and intents. By employing many of the principles and warnings in this chapter, we can hope to remain balanced in our code building. For the previous example of a GalleryComponent, we should, once again, explore the requirements of the abstraction:
These, we can say, are the levers that we must provide to the underlying cross-platform complexity. Our abstraction should solely aim to expose these levers and no other unnecessary complexity. The following is an example of such an abstraction:
const gallery = new GalleryComponent([
{
src: '/foo/images/PictureOne.jpg',
caption: 'The Caption',
width: 200,
height: 150
},
{
src: '/foo/images/PictureTwo.jpg',
caption: 'The Caption',
width: 200,
height: 150
},
]);
Via this interface, we can define one or more images, set their dimensions, and define captions for each image. It fulfills all of the requirements without inviting new complexities or leaking complexities from the underlying implementation. This is, therefore, a balanced abstraction.
JavaScript allows us to program in a variety of different ways. Many of the examples we've shared so far in this book have been more inclined towards OOP, which primarily uses objects to express problem domains. Functional programming is different in that it uses mostly pure functions and immutable data to express problem domains.
It's easiest to observe and discuss the difference between OOP and functional programming by exploring an example. Let's imagine that we wish to build a mechanism so that we can fetch paginated data from a server. To achieve this in an object-oriented way, we might create a PaginatedDataFetcher class:
// An OOP approach
class PaginatedDataFetcher {
constructor(endpoint, startingPage) {
this.endpoint = endpoint;
this.nextPage = startingPage || 1;
}
getNextPage() {
const response = fetch(
`/api/${this.endpoint}/${this.nextPage}`
);
this.nextPage++;
return fetched;
}
}
The following is an example of how you would use the PaginatedDataFetcher class:
const pageFetcher = new PaginatedDataFetcher('account_data', 30);
await pageFetcher.getNextPage(); // => Fetches /api/account_data/30
await pageFetcher.getNextPage(); // => Fetches /api/account_data/31
await pageFetcher.getNextPage(); // => Fetches /api/account_data/32
As you can see, with each call to getNextPage, we retrieve the next page of data. The getNextPage method relies on the remembered state of its objects, endpoint and nextPage, in order to know which URL to request next.
Functional programming, as distinct from OOP, is purely interested in the usage of functions and immutable state to achieve its goals. The first mental blocker that people usually encounter when exploring functional programming is related to states, raising questions such as Where should I store my state? and How do I make things change without being able to mutate that state? We can explore this question by looking at the functionally programmed equivalent of the paginated data fetcher.
We have created a function, getPage, to which we will pass an endpoint and a pageNumber, as follows:
// A more functional approach
const getPage = async (endpoint, pageNumber = 1) => ({
endpoint,
pageNumber,
response: await fetch(`/api/${endpoint}/${pageNumber}`)
next: () => getPage(endpoint, pageNumber + 1)
});
When called, the getPage function will return an object containing the response from the server, as well as the endpoint and pageNumber used. In addition to these properties, the object will also contain a function named next, which itself, if called, will fire off another request via a subsequent call to getPage. It can be used in the following way:
const page1 = await getPage('account_data');
const page2 = await page1.next();
const page3 = await page2.next();
const page4 = await page3.next();
// Etc.
You'll notice that, when using this pattern, we only need a reference to the last retrieved page in order to make the next request. Page 3 is retrieved via page 2's returned next() function. Page 4 is retrieved via page 3's returned next() function.
Our getPage function does not mutate any data: it only uses the passed data to derive new data, and therefore, it can be said that it employs immutability. It can also be said that it is a pure function as well, in that, with a given set of input parameters (an endpoint and a pageNumber), it will always return the same thing. The next function returned by getPage is also pure, as it will always return the same thing: if I call page2.next() a million times, it'll always fetch page 3.
Functional purity and immutability are among the most vital functional concepts to understand, and, usefully, are principles that are applicable to all paradigms of programming. We're not looking to thoroughly explore functional programming here, but just to cover its most applicable principles in order to bolster our abstraction-crafting abilities.
Functions can be said to be pure when their return value is only derived from their input values (also called idempotence), and when there are no side-effects. These characteristics give us the following benefits:
Often in OOP, methods on objects cannot be said to be pure since they mutate the state (on the object) or return different results with the same input parameters. Consider, for example, the following Adder class:
class Adder {
constructor() {
this.total = 0;
}
add(n) {
return this.total += n;
}
}
const adder = new Adder();
adder.add(10); // => 10
adder.add(10); // => 20
adder.add(5); // => 25
The add method here is not pure. It returns different results even when given the same arguments, and it has a side-effect: mutating a state that it does not own (that is the total property of the object). We could instead create a functionally pure addition abstraction very simply:
const add = (a, b) => a + b;
add(10, 10); // => 20
add(10, 20); // => 30
This may seem contrived, but the concept behind functional purity is to, from complex needs, derive the truly pure primitives and functions that are needed to construct it. Functional purity teaches us a general lesson here: to break down functionality to its most primal parts until you have a truly testable standalone unit. We can then compose these smaller units into larger units that do more complex work.
This chapter has largely been about how we structure and separate our abstractions, but it is equally important to consider the expectations of the data that passes between these abstractions.
Immutability refers to the simple idea that data should not mutate. This means that, when we initialize an object, for example, we should not add new properties to it or change existing properties over time. Instead, we should derive a brand new object and only make changes to our own copy. Immutability is a characteristic of data, but is also a general tenet of functional programming. A language can also enforce immutability by disallowing the mutation of already declared variables or objects. JavaScript's const is an example of this type of enforcement:
const name = 'James';
name = 'Samuel L. Jackson';
// => Uncaught TypeError: Assignment to constant variable.
Knowing that something is immutable means that we can rest assured that it will not change; we can rely on its characteristics without worrying that some other part of the program may change it without us knowing. This is especially pertinent in the asynchronous world of JavaScript, where data is shuttled between scopes and interfaces in complex ways.
Like many of the principles we've covered in this chapter, immutability does not have to be followed religiously to gain benefits from it. Immutability in some areas, and mutability in others, can be a viable approach. Imagine an official document being shuttled around a government building. Each department has the implicit assumption that the document has not been arbitrarily modified by unexpected people; a specific department may choose to make a copy of the document and then make various mutations to its own copy for its own unique purposes. A codebase is not so different from this. By crafting abstractions and letting them interdepend on each other, we are intentionally enabling them to manipulate each other's data and functionality.
In this chapter, we covered a vast amount of theory and practical skills. We covered the LoD (or principle of least information), all SOLID principles, the principle of abstraction, and a couple of key principles from the paradigm of functional programming. Even if you don't remember all of the names, you will hopefully remember the underlying knowledge and key lessons that each principle encapsulates.
Programming is as much an art as it is a science. It involves balancing all of these principles in the pursuit of crafting truly balanced abstractions. None of these principles should be considered hard-and-fast rules. They are merely guidelines that will help us on our journey.
In the next chapter, we continue this journey by exploring one of the most challenging aspects of programming, both in JavaScript and outside it: the problem of naming things.
Names are everywhere. They are our mind's way of abstracting the complexity of the universe. In the world of software, we are always engaged in crafting new abstractions to describe our everyday realities. A common quip in the programming world is naming things is hard. Coming up with a name isn't always hard, but coming up with a good name usually is.
In the previous chapters, we have explored the principles and theory underlying abstractions. In this chapter, we'll provide the final key to the puzzle. An abstraction cannot be a good abstraction without good naming. In the names we use, we are distilling a concept, and that distillation will define how people end up understanding the concept. So, naming things isn't just the provision of arbitrary labels; it is the provision of understanding. Only via a good name can a user or other programmer be able to internalize our abstraction fully and navigate it with a full understanding.
In this chapter, we will use some examples to explore the key characteristics that make a good name. We'll also discuss the challenges of naming things in a dynamically typed language such as JavaScript. We should come away from this chapter with a clear understanding of what is involved in coming up with clean and descriptive names.
Specifically, we'll be covering the following topics:
Breaking down the key elements of a good name is difficult. It seems to be more of an art than a science. The boundary between quite a good name and a very good name is fuzzy and liable to subjective opinions.
Consider a function that is responsible for applying multiple CSS styles to a button. Imagine a scenario in which this is a standalone function. Which of the following names would you consider to be the most suitable?
You've likely picked your favorite. And there is, among those of you reading this book, bound to be disagreements. Many of these disagreements will be founded in our own biases. And many of our biases will have been conditioned by factors such as what language we speak, what programming languages we've been previously exposed to, and what types of programs we spend our time creating. There are many variances that exist between all of us and yet, somehow, we have to come up with a non-fuzzy concept for what a good or clean name is. At the very least, we can say that a good name might have the following characteristics:
This doesn't completely cover the complexity of naming, but with these three characteristics, we have a starting point. In the remainder of this section, we will learn how each of these characteristics is vital to the process of naming things.
A good name indicates purpose. Purpose is what something does, or what something is. In the case of a function, its purpose is its behavior. This is why functions are typically named in the verbal form, such as getUser or createAccount, whereas things that store values are usually nouns, such as account or button.
A name that encapsulates a clear purpose will never need further explanation. It should be self-evident. If a name requires a comment to explain its purpose, then that is usually an indicator that it has not done its job as a name.
The purpose of something is highly contextual and so will, therefore, be informed by the surrounding code and the area of the codebase in which that name resides. This is why it's often okay to use a generic name as long as it is surrounded by context that helps to inform its purpose. For example, compare these three method signatures within the TenancyAgreement class:
class TenancyAgreement {
// Option #1:
saveSignedDocument(
id,
timestamp
) {}
// Option #2:
saveSignedDocument(
documentId,
documentTimestamp
) {}
// Option #3:
saveSignedDocument(
tenancyAgreementSignedDocumentID,
tenancyAgreementSignedDocumentTimestamp
) {}
}
There are subjectivities to this, of course, but most people would agree that, when we have a surrounding context that communicates its purpose well, we shouldn't need to granularize the naming of every variable within that context. With this in mind, we can say that Option #1 in the preceding code is too limited and may invite ambiguity and that Option #3 is needlessly verbose as parts of its argument names are already provided by its context. Option #2, however, with documentId and documentTimestamp, is just right: it sufficiently communicates the purpose of the arguments. And this is all we need.
Purpose is absolutely central to any name. Without a description or an indication of purpose, a name is merely decoration, and can often mean that users of our code are left rummaging around between documentation and other pieces of code just to figure something out. Therefore, we must remember to always consider whether our names communicate purpose well.
A good name indicates concept. A name's concept refers to the idea behind it, the intent in its creation, and how we should think about it. For example, a function named relocateDeviceAccurately not only tells us what it will do (its purpose) but informs us about the concept surrounding its behavior. From this name, we can see that devices are things that can be located and that locating such devices can be done at different levels of accuracy. A relatively simple name can arouse a rich concept within the minds of those who read it. This is part of the vital power of naming things: names are avenues to understanding.
A name's concept, like its purpose, is strongly tied to the context in which it exists. Context is the shared space that our names exist within. The other names that surround the name we're interested in are absolutely instrumental in helping us understand its concept. Imagine the following names together:
By these names, we immediately understand that a deal is something that can have at least four different states. It is implied that these states are mutually exclusive and cannot apply to a deal at the same time, although that is unclear at this time. We are likely to assume that there are specific conditions related to whether a deal is pending or stalled, although we're not sure what those conditions are. So, even though there is ambiguity here, we are already starting to build up a rich understanding of the problem domain. That's just by looking at names—without even reading the implementation.
We have spoken about context as a kind of shared space for names. In programming vernacular, we usually say that things named together in one area occupy a single namespace. A namespace can be thought of as a place where things share a conceptual area with each other. Some languages have formalized the concept of a namespace into its own language construct (often called a package, or simply a namespace). Even without such formal language constructs, JavaScript still makes it possible to construct namespaces via hierarchical constructs such as objects like so:
const app = {};
app.transactions = {};
app.transactions.dealMaking = {};
app.transactions.dealMaking.states = [
'REJECTED_DEAL',
'ACCEPTED_DEAL',
'PENDING_DEAL',
'STALLED_DEAL'
];
Most programmers tend to think of namespaces as a very formal construct, but this isn't often the case. Often, without knowing it, we are composing implied namespaces when we write functions with functions within them. Instead of being delineated by a level of an object hierarchy, the namespaces, in this case, are delineated by the scopes of our functions, as follows:
function makeFilteredRequest(endpoint, filterFn) {
return fetch(`/${endpoint}/`)
.then(response => response.json())
.then(data => data.filter(filterFn);
}
Here, we are making a request to an endpoint, via fetch, and before we return, we are gathering the required data via tapping into the promise returned by fetch. To do this, we use two then(...) handlers.
Our first then(...) handler names its argument response, while the second one names its argument data. Outside the context of makeFilteredRequest, these terms would be very ambiguous. However, because we are within the implied namespace of a function related to making a filtered request, the terms response and data are sufficient to communicate their concepts.
The concepts communicated by our names, much like their purposes, are heavily intertwined with the contexts in which they are specified, so it's important to consider not only the name itself but everything that surrounds it: the complex mesh of logic and behavior in which it resides. All code deals with some level of complexity, and a conceptual understanding of that complexity is crucial in being able to harness it. So, when naming something, it helps to ask yourself: How do I want them to understand this complexity? This is relevant if you're crafting a simple interface to be consumed by other programmers, writing a deeply embedded hardware driver, or creating a GUI for non-programmers to consume.
A good name indicates a contract with other parts of the surrounding abstraction. A variable, by its name, may indicate how it will be used or what type of value it contains and what general expectations we should have about its behavior. It's not usually thought about, but when we name something, we are, in fact, setting up a series of implicit expectations or contracts that will define how people understand and use that thing. Here are some examples of the hidden contracts that exist in JavaScript:
Whether we like it or not, all names carry with them the baggage of unavoidable expectations regarding their values and behaviors. It's important to be aware of these conventions so that we do not accidentally break the contracts that other programmers rely on. Every convention will have an exception where it doesn't apply, of course, but nonetheless, we should try to abide by them where possible.
Unfortunately, there isn't a canonical list where all of these contracts have been defined. They are usually quite subjective and will depend on the code base. Nonetheless, where we do encounter such conventions, we should follow them. As we mentioned in Chapter 2, The Tenets of Clean Code, ensuring familiarity is a great way to increase the maintainability of our code. And there is no better way to ensure familiarity than to adopt conventions that other programmers have come to adopt.
Many of these implied contracts are related to types, and JavaScript, as you may be aware, is dynamically typed. This means the types of values will be determined at runtime, and the type contained by any variable may be liable to change:
var something;
something = 1; // a number
something = true; // a boolean
something = []; // an array
something = {}; // an object
The fact that a variable can refer to many different types means that the contracts and conventions implied by the names we adopt are even more important. There is no static type checker to help us. We are left alone at the chaotic whim of ourselves and other programmers.
Contracts are not only important because of JavaScript's dynamically typed nature. They are fundamentally useful in giving us confidence in how certain values behave and what we can expect from them throughout the runtime of our program. Imagine if there was an API with a method called getCurrentValue() that didn't always return the current value. That would break its implied contract. Seeing names through the lens of contracts is quite a mind-warper. Soon, you will begin to see contracts everywhere – contracts between variables, between interfaces, and at the integration level between entire architectures and systems.
Now that we've discussed the three characteristics of a good name (purpose, concept, contract), we can begin to explore some anti-patterns, that is, ways of naming things that we should try to avoid.
Much like the abstraction-building warnings of DRY and YAGNI, naming has its own warnings and anti-patterns. There are many ways to compose a bad name, and nearly all of them can be split into three broad naming anti-patterns: needlessly short names, needlessly exotic names, and needlessly long names.
Names are the initial lenses via which we and others will view the abstractions we build. Therefore, it is vital to know how to avoid creating lenses that only end up obscuring understanding and complicating things for other programmers. Let's begin by exploring needlessly short names and how they can end up drastically limiting our ability to understand what something does.
Names that are too short are usually employing either program-specific knowledge or domain-specific knowledge that may not generalize well to the audience of the code. A lone programmer may think it reasonable to write the following code:
function incId(id, f) {
for (let x = 0; x < ids.length; ++x) {
if (ids[x].id === id && f(ids[x])) {
ids[x].n++;
}
}
}
We are able to discern the fact that it is related to IDs and its purpose is to conditionally increment a specific object's n property within the ids array. Therefore, it is possible to discern what it is doing on a functional level, but its meaning and intent are difficult to grasp. The programmer has used single-letter names (f, x, n) and has also employed an abbreviated function name (incId). Most of these names fail to fulfill the basic characteristics that we desire from a name: to indicate purpose, concept, and contract. We can only guess at these names' purposes and concepts by how they are being used. It would vastly help to refactor this with more meaningful names:
function incrementJobInstancesByIdIfFilter(id, filter) {
for (let i = 0; i < jobs.length; i++) {
let job = jobs[i];
if (job.id === id && filter(job)) {
job.nInstances++;
}
}
}
We now have a far clearer idea of what's going on. The arrays being iterated over contains jobs. The function's purpose is to find jobs with a specified ID and conditional on that job satisfying a specified filter. It increments the job's nInstances property by 1. Via these new names, we already have a far richer conceptual understanding of this abstraction. We now understand that jobs are items that can have any number of instances and that the number of current instances is tracked via the nInstances property. Via the lenses provided by the names, we have been able to understand the underlying problem domain more clearly. Now, we can see that names are not just decoration or needless verboseness; names are the very essence of your abstractions.
A needlessly short name is, in many ways, just an insufficiently meaningful name. However, a name being short does not necessarily indicate a problem. The iterator variable, i, which we used in the preceding code, is perfectly fine as it is a convention that has established itself over decades. Programmers all over the world understand the conceptual and contractual implications of it: it is used only to iterate through an array and to access array elements at each stage of the iteration.
On the whole, and outside of rare exceptions such as our iteration variable, it is incredibly important to avoid the deficit in meaning that is invited by short names. They are often composed initially with haste or laziness and may even give the programmer attuned to their meaning a sense of accomplishment. After all, being able to wield obscure logic is a gift for the ego. But as we've covered, the ego is not a friend to clean code. Whenever you feel the urge to use a short name, push back on the impulse and take the time to pick a name that is richer in meaning.
Another avenue for the ego is in the proliferation of exotic names. Exotic names are those that draw unnecessary attention to themselves and are often obscure or elusive in meaning, like so:
function deStylizeParameters(params) {
disEntangleParams(params, p => !!p.style).obliterate();
}
This is an ostensibly simple piece of behavior obscured by needlessly exotic names. We can, with minimal effort, make a world of difference to the comprehensibility of these abstractions with only a couple of tweaks:
function removeStylingFromParams(params) {
filterParams(params, param => !!param.style).remove();
}
Names, on the whole, should be boring. They should not draw attention to themselves. They should sit there with only their plain meaning on display and nothing that makes fellow programmers go, oh that's what it means! or hehe clever! Our egos may have their own ideas about naming, but we should remember to limit the ego and think purely of the people who must endure the task of trying to understand our code and the interfaces we've created. On the whole, the following advice will keep us on the right track:
Being overly exotic risks alienating our audience. You may be able to easily understand the names you've adopted, but that does not mean they are easily understood by others. The wider programming community is incredibly diverse and has many different cultural and linguistic backgrounds. It's best to stick to names that are descriptive and boring so that your code is understandable to as many people as possible.
As we've already discovered, the needlessly short name is, in fact, a name without sufficient meaning. The needlessly long name is, therefore, a name with too much meaning. You may wonder how a name could have too much meaning. Meaning is a good thing, but too much meaning crushed into a single name can only serve to confuse; for example:
documentManager.refreshAndSaveSignedAndNonPendingDocuments();
This name is hard to understand: is it refreshing and saving documents that are signed and documents that are non-pending, or is it refreshing and saving documents that are both signed and non-pending? It's unclear.
This long name gives us a clue that the underlying abstraction is needlessly complex. We can split the name into its constituent parts to get a full grasp of its interface:
We have a few different things happening here. With names this long, a good guideline is to refactor the underlying abstraction so that we only need a name with, at most, one verb, one adjective, and one noun. For example, we could take our long name and split its function into four distinct functions:
documentManager.refreshSignedDocuments();
documentManager.refreshNonPendingDocuments();
documentManager.saveSignedDocuments();
documentManager.saveNonPendingDocuments();
Alternatively, if the intent is to perform actions on documents that carry multiple states (SIGNED and NON_PENDING), then we could implement a method like this for refreshing (and a similar one for the saving action):
documentManager.refreshDocumentsWithStates([
documentManager.STATE_SIGNED,
documentManager.STATE_NON_PENDING
]);
The point is that long names are a clue to a broken or confused abstraction. Making a name more understandable usually goes hand in hand with making an abstraction more understandable.
As with short names, the problem is not the length of the name itself: it is what the length usually indicates. With long names, what is indicated is crushing too much meaning into a single name, indicating a confused abstraction.
So far, we've talked about the three most important characteristics of a name: purpose, concept, and contract. One of the easiest ways to bestow these characteristics upon your names is to use consistency and hierarchy to your benefit. Consistency here refers to using the same pattern of naming across many different names within a given area of code. Hierarchy, on the other hand, refers to the way we structure and put together different areas of code to form a holistic architecture. Together, they allow us to give a name a rich context that can be used to make strong inferences about its purpose, concept, and contract.
This is best explained by looking at the JavaScript directory of a fictional app. We have a directory full of files, like so:
app/
|-- deepClone.js
|-- deepEquality.js
|-- getParamsFromURL.js
|-- getURL.js
|-- openModal.js
|-- openModalWithTemplate.js
|-- setupAppWithCustomConfig.js
|-- setupAppWithDefaultConfig.js
|-- setURL.js
|-- ...
There is no hierarchy, so we can only discern context from the names themselves and what they appear to relate to. For example, there is a getURL and a setURL file, which both presumably relate to URLs and could be considered utilities. It would, therefore, be helpful to have these occupy the same part of the hierarchy or a shared namespace, such as app/utils/url. We can also refactor other parts of our directory structure into a more contextually rich hierarchy:
app/
|-- setup/
| |-- defaultConfig.js
| |-- setup.js
|-- modal/
| |-- open.js
| |-- openWithTemplate.js
|-- utils/
|-- url/
| |-- getParams.js
| |-- get.js
| |-- set.js
|-- obj/
|-- deepEquality.js
|-- deepClone.js
Immediately, things are clearer. The cognitive strain of understanding all of those files and what they do is now lessened by each file having its own rich context. You'll also notice that we've been able to simplify the names at various parts of the hierarchy; for example, we have renamed openModal.js to modal/open.js. This is an additional benefit of employing hierarchies of names: at each level of naming, we can simplify and shorten the names, lessening comprehension time.
Just like we have provided meaning via the hierarchy of a directory structure, we can also provide meaning within the code itself. Within a function, for example, the names within will naturally receive a lot of their context from the function's name itself and its situation within a larger module. Consider how it would be quite unusual to write code like this:
function displayModalWithMessage(
modalDisplayer_Message,
modalDisplayer_Options
) {
const modalDisplayer_ModalInstance = new Modal();
modalDisplayer_ModalInstance.setMessage(modalDisplayerMessage);
modalDisplayer_ModalInstance.setOptions(modalDisplayerOptions);
modalDisplayer_ModalInstance.show();
return modalDisplayer_ModalInstance;
}
The names within the function are needlessly prefixed with contextual information (such as modalDisplayer_...) that the reader of the code can already get from the function itself. Typically, we write code that takes advantage of where a variable sits and the meaning that it gets from its context. It would be far more normal for the preceding code to appear like so:
function showModalWithMessage(message, options) {
const modalInstance = new Modal();
modalInstance.setMessage(message);
modalInstance.setOptions(options);
modalInstance.show();
return modalInstance;
}
In an previous chapter, we discussed the principle of abstraction and how the implementation of a module should be independent of its interface. We can see that this principle is expressed in this function. The scope of a function (its implementation) should be entirely independent (and even ignorant!) of its interface. So, arguably, it's not the business of the modalInstance variable to know which function it resides in and so the former naming technique, which prefixed it with modalDisplayer_..., would be in violation of the principle of abstraction.
Thinking about hierarchies in terms of abstraction is key. Hierarchies aren't just useful from an organizational perspective. They should, ideally, be a reflection of the layers of abstraction that reside within our code. Higher-level abstractions are at the top of the hierarchy, and the deeper we go into the hierarchy, the more low-level we will get. This is a good general rule to adopt: make your hierarchy reflect your abstractions.
Naming things with consistency complements this rule. Within a single layer of our abstraction, that is, within a single level of the hierarchy, we should adopt common naming patterns so that the reader of our code can easily navigate and understand its concepts. For example, if we are creating an interface that will be used to add and remove items from a data structure, then we should avoid naming similar actions in an inconsistent manner. Consider the following class schematic:
class MyDataStructure {
addItem() {}
pushItems() {}
setItemIfNotExists() {}
// ...
}
Very confusingly, this abstraction is offering up three different variations of the concept of adding to the data structure: adding, pushing, and setting. These names are all, in fact, referring to the same concept, so we should adopt a common naming pattern, such as the following:
class MyDataStructure {
addItem() {}
addItems() {}
addItemIfNotExists() {}
// ...
}
This interface is now far easier to understand. There is less ambiguity and less cognitive burden in using it. As a user of this abstraction, I would no longer need to remember whether I should be using add, set, or push. Consistency is a characteristic that results from the avoidance of needless differences. Inconsistencies are jarring and so they should only be used to demarcate genuine functional or conceptual differences.
JavaScript, due to its ever-changing nature, has gathered a huge variety of conflicting conventions. Many of these conventions garner strong opinions either in support or in disapproval. We have, however, settled on some basic conventions around naming that are more or less globally accepted:
Apart from these foundational conventions, the decision of naming is left largely up to the creativity and skill of the programmer. The names you end up employing will be largely defined by what problems you're solving. Most code will inherit naming conventions from the APIs it interfaces with. Using the DOM API, for example, will usually mean that you adopt names such as element, attribute, and node. Many of the popular frameworks available will tend to dictate the names we adopt as well. It is absolutely useful and necessary to adopt such conventional paradigms from the ecosystem that you're working in, but it's also useful to have some foundational techniques and concepts under your belt so that you can craft beautifully named abstractions, even in new and alien problem domains.
JavaScript is a dynamically typed language, meaning that the type of a value will be determined at runtime and that the type contained by any variable may be liable to change during runtime. This is in contrast to statically-typed languages, which have compile-time warnings related to your usage of types. The implication of this is that, as JavaScript programmers, we need to be much more careful in the way we employ types and how we name our variables.
As we know, when we name things, we are implying a contract. This contract will define how other programmers make use of that thing. This is part of the reason why, in various languages, something called Hungarian notation has been very popular. It involves including type annotations in a name itself, like so:
Hungarian notation is useful for the following reasons:
However, it also has the following disadvantages:
In the landscape of JavaScript, we see Hungarian notation used in a few places: the most common is when naming a variable that may refer to a DOM element. The notations for these names will usually be in the form elHeader, headerEl, headingElement, or even $header. The latter, with a dollar prefix, is most famously used in the jQuery library. Its fame there led to it being a standard in various other places. Chromium DevTools, for example, employs a dollar prefix for element references and methods related to querying the DOM (for example, $$(...) is aliased to document.querySelectorAll(...)).
Hungarian notation is something that can be utilized partially, where you're concerned there may be ambiguity. For example, you can use it where you have both a complex type and a primitive type referring to the same concept within a single scope:
function renderArticle(name) {
const article = Article.getByName(name);
const title = article.getTitle();
const strArticle = article.toString();
// ...
}
Here, we have an article variable that refers to an instance of the Article class. In addition to this, we also want to use a string representation of our article. To get around a potential naming conflict, we have used an str prefix to indicate that the variable refers to a string value. In isolated cases like these, Hungarian notation can be useful. You don't need to use it exhaustively, but it's a useful tool to have up your sleeve.
Most abstractions you end up crafting in JavaScript will be manifested within functions. Even within grand architectures, it is individual functions and methods that do the work, and it is in their conception that a good abstraction begins to reveal itself. It is, therefore, worth thinking quite deeply about how we should name our functions and what factors we should take into consideration when doing so.
A function's name should typically use what, in grammar, is called the imperative form. The imperative form is what we employ when we are giving instructions, such as walk to the shop, buy bread, stop there!.
The direct nature of the imperative form is the most understandable and readable in the context of programming. To find the correct imperative form for your specific problem, it's best to imagine the act of giving a military order, that is, don't mince your words and say exactly what it is that you want to occur:
Often, we wish to qualify our instructions. If you were to issue an instruction to a person, such as find my bicycle, you would likely further qualify that instruction with information such as it's blue and it's missing its front wheel. It is important, however, not to let a function's name get bogged down with these qualifications. The following function would be an example of this:
findBlueBicycleWithAMissingFrontWheel();
As we mentioned earlier, a needlessly long name is a sign of a bad abstraction. When we see this type of over-qualification, we should take a step back and reconsider. Here, it's important to draw a line between what is sensible in spoken language and what is sensible when programming. In programming, functions are ways of abstracting common behavior that can be adjusted or configured, as needed, via arguments.
So, it is via arguments that we should be expressing the qualifications of blue and missing front wheel. And we could, for example, express these as a single object argument like so:
findBicycle({
color: 'blue',
frontWheel: 'missing'
});
By moving the qualifying parts of a function's name into its arguments, we are producing a cleaner and more comprehensible abstraction. This has the added benefit of increasing the configurability of the abstraction, thereby providing the user with more possibilities.
In our case, we may wish to give users the ability to find objects other than bicycles. To cater to this, we would make the name of the function more generic (for example, findObject) and shift the qualifier to the arguments by adding a new option property (for example, type), like so:
findObject({
type: 'bicycle',
color: 'blue',
frontWheel: 'missing'
});
Something curious happens at this stage of the process. We have, correctly, moved our various qualifiers to the arguments of our function, expanding the usefulness and configuration of our abstraction. But now what we have is an abstraction that is doing many things, so at some point, it may be prudent to take a step back and build higher-level abstractions to encapsulate these different behaviors. In our case, we could achieve this via functional composition, like so:
const findBicycle = config => findObject({ ...config, type: 'bicycle' });
const findSkateboard = config => findObject({ ...config, type: 'skateboard' });
const findScooter = config => findObject({ ...config, type: 'scooter' });
Above all, a function is a unit of behavior. As the SRP tells us, it's important to ensure that they are only doing one discernible thing. When considering these things or units of behavior, it's important to think about what a function does from the perspective of those who'll use it. Technically, it's highly likely that our composed findScooter function does all manner of things beneath the surface. It may be incredibly complex. But at the layer of abstraction where it will be used, it can be said to only do one thing, and that is what's important.
If you're ever stuck for a name, there's a clever approach you can use to unstick yourself. When you have an abstraction or variable that needs a name, look carefully at what it does or what it contains and then come up with at least three bad names that describe it. Don't worry about the abstraction or interface you wish to provide for now; just imagine you were describing the functionality to someone who doesn't know anything about the codebase. Be direct and descriptive.
For example, let's say we're embedded in the part of the code base that deals with setting up new user-names. We need to check that the username does not match a set of specifically forbidden words, such as admin, root, or user. We want to write a function to do this but we're not sure what name to pick. So, we decide to try the three bad names approach. This is what we come up with:
Coming up with three less-than-perfect names is a lot easier than spending many minutes trying but failing to come up with the perfect name. It doesn't matter how bad these three names are. What's important is that we can come up with at least three. Now, having seeded the set of possibilities, we're free to compare and contrast the names we've found and mix and match them to find the most descriptive and direct way of describing the purpose of our function. In this case, we may have eventually decided on a name adapted from those three possibilities: isUsernameForbiddenWord. We wouldn't have got there if it wasn't for the three bad names approach.
In this chapter, we have wrestled with the difficult art of naming things. We've discussed the characteristics of a good name, that is, purpose, concept, and contract. We've walked through examples of how to weave these characteristics into our names and what anti-patterns to steer clear of. We've also discussed the importance of hierarchy and consistency in our pursuit of clean abstractions. Finally, we have covered several helpful techniques and conventions that we can utilize when we're having a difficult time naming things.
In the next chapter, we will, at last, begin to delve into the innards of the JavaScript language itself and learn how to wield its constructs and syntax in a way that yields truly clean code.
In this section, we'll do a deep dive into JavaScript's internals and language constructs. This will give us a really solid foundational understanding of how to use JavaScript's best parts to craft clean code.
This section contains the following chapters:
So far, we have explored the meaning of clean code from several different perspectives. We've explored how the code we write allows our users to wield remarkable complexity by leveraging abstractions. We've gone on to discuss the tenets of clean code, such as reliability and usability, and the various traps and challenges to watch out for when pursuing these goals.
In this chapter, we'll be exploring the JavaScript language itself, in great detail, including both the more common language constructs and the more obscure and confusing aspects. We'll be applying our accrued wealth of knowledge about clean code to all these parts of the language and will build an understanding of JavaScript that's tailored purely to the creation of clean code.
We'll begin by looking at the most atomic part of JavaScript: the primitive values that serve as the building blocks for any program. Then, we'll move on to non-primitive values, known as objects. In our exploration of these types, we will, through examples, be exposing the semantics that make each type unique and the pitfalls to avoid in their usage. The crucial knowledge that we'll gain in this chapter will be applied in later chapters as we build up a truly complete knowledge of what it means to write clean code in JavaScript.
By the end of this chapter, you should feel comfortable in the following topic areas:
A primitive type in JavaScript is any value that is not an object and thus does not have any methods or properties. There are seven primitive types in JavaScript:
In this section, we'll explore the common characteristics among these primitives and delve into each individual type to explore how it works and what potential hazards exist in its usage. We'll gain an appreciation for how the JavaScript language itself is just a set of distinct abstractions that, when wielded masterfully, can make easy work of any problem domain.
All primitive values are immutable, meaning that you cannot mutate their values. This is a core part of their primitiveness. You cannot, for example, change the number value of 3.14 to 42, or change the value of a string to its uppercased variation.
When we reassign a variable, giving it a new value, we are not changing the value itself; we are only changing which value the variable refers to, as shown here:
let name = 'simon';
let copy = name;
// Assign a new value to `name`:
name = name.toUpperCase();
// New value referred to by name:
name; // => "SIMON"
// Old value remains un-mutated:
copy; // => "simon"
Note how copy has remained lowercase. The primitive value simon has not been mutated; instead, a new primitive value has been derived from it, via the toUpperCase method, and then assigned to the variable that previously held the lowercase variant.
You'll remember that we mentioned that primitive values don't have methods as they are not objects. So, how exactly are we able to call toUpperCase on the preceding string? Is that not a method? Yes, it is. And to allow us to access this method, JavaScript wraps primitive values in their respective wrapper objects at the time of property access. This occurs for all primitive values, apart from null and undefined.
Primitive values, in these moments of being wrapped, remain immutable but, via their wrapped instance, provide access to properties and methods. A string value would be wrapped in a String instance, while a number value would be wrapped in a Number instance. The same would occur for all other non-null and non-undefined primitives. You are free to instantiate these wrapper objects yourself: you will observe that they no longer behave like primitives, though; they are objects, and, as such, you can add and mutate properties on them:
const name = new String('James');
// We can add arbitrary properties, since it is an object:
// (Warning: this is an anti-pattern)
name.surname = 'Padolsey';
name.surname; // => "Padolsey"
Invoking a wrapper constructor (for example, Number, String, and so on) as a regular function has a unique behavior. Instead of returning a new wrapper instance, it will cast the value to a particular type and return a regular primitive. This is quite useful when you're casting one type to another:
// Cast a number to a string:
String(123); // => "123"
// Cast a string to a number
Number("2"); // => 2
// Cast a number to a boolean
Boolean(0); // => false
Boolean(1); // => true
Invoking wrapper constructors as functions, as we have done here, is a useful casting technique, though it's not the only one. We'll cover typecasting and coercion in a lot more detail in Chapter 7, Dynamic Typing.
In JavaScript, all the values in Boolean contexts will evaluate to either true or false. To describe this behavior, we usually refer to values as either truthy or falsy. To determine the truthiness of a value, we can simply pass it to the Boolean function:
Boolean('hi'); // => true
Boolean(0); // => false
Boolean(42); // => true
Boolean(0.1); // => true
Boolean(''); // => false
Boolean(true); // => true
Boolean(null); // => false
There are only eight falsy values in JavaScript, and all of them are primitive types:
All values that are not falsy are, therefore, truthy. Throughout this and the next chapter, we will be exploring the implications of these truthy and falsy values. For now, it's only important to know that the preceding falsy values will, when used in conditional or logical contexts, behave as if they were false. A falsy value, when used in an if statement, for example, would act the same as if it were false:
if (0) {
// This will not run. 0 is falsy.
}
if (1) {
// This will run. 1 is truthy.
}
The existence of these falsy values means that we must be wary of how we check for certain conditions. It's easy to fall into the trap of testing for the existence of a certain value state by using only its truthiness to determine existence. For example, let's say that we need to be able to check for the age of a person:
if (person.age) {
processIdentity(person);
}
This is a contrived example, but we can imagine a system in which the identities of individuals need to be processed somehow, perhaps through a medical application. Checking for the existence of the age property is not going to do what's intended if the age happens to be 0. Perhaps the system needs to cater to the eventuality of a newborn baby being entered into the system, but suddenly it breaks because the age is 0. In such scenarios, it's best to be preemptively explicit, even if you don't expect odd falsy values. In this context, we likely want to check for either null or undefined, so we should explicitly do that:
if (person.age === null || person.age === undefined) {
processIdentity(person);
}
This code is far more resilient to the possible variabilities of the age property. We could also, perhaps, be more in line with our requirements and check only for the specific characteristics that we are interested in, such as that the age property is a number within specific bounds. The point is that it's better to be explicit in Boolean contexts such as if statements so that you don't run into an unexpected falsy value.
The number primitive type is used to express numerical data. It stores this data in the double-precision 64-bit floating-point format (IEEE 754). 64 bits here refers to there being 64 binary digits available to store information. The entire 64-bit format that's used in the IEEE 754 standards can be broken down into three chunks:
Technically, there are 53 bits available (not 52) for the expression of an integer value as the leading bit of the significand field resides within the exponent field. This is an important clarification as it has a direct effect on how much precision we can get from JavaScript numbers. Having 53 bits available to express an integer value means that any numbers greater than 253-1 are considered unsafe. These safety limits are available as constants on the Number object:
The loss of precision beyond these bounds can be observed if we try to perform addition on the upper limit:
const max = Number.MAX_SAFE_INTEGER;
max + 1; // => 9007199254740992 (correct)
max + 2; // => 9007199254740992 (incorrect)
max + 3; // => 9007199254740994 (correct)
max + 4; // => 9007199254740996 (incorrect)
// ... etc.
Here, we can see that the evaluated additions are incorrect. Beyond MAX_SAFE_INTEGER, all mathematical operations will be similarly imprecise.
To sum this up, any values between Number.MIN_SAFE_INTEGER and Number.MAX_SAFE_INTEGER are safe to use and will provide integer precision, while values beyond these bounds should be considered unsafe. If we feel ourselves needing an integer outside of these bounds, then we can use JavaScript's BigInt primitive:
const max = BigInt(Number.MAX_SAFE_INTEGER)
max + 1n; // => 9007199254740992n (correct)
max + 2n; // => 9007199254740993n (correct)
max + 3n; // => 9007199254740994n (correct)
max + 4n; // => 9007199254740995n (correct)
// ... etc.
We'll explore the BigInt primitive further in a later part of this section. For now, just remember to always consider the largeness of your numbers and whether they can be fully accommodated by JavaScript's Number type. It's also important to consider the precision of decimal values (such as in fractions) as well. When expressing decimals in JavaScript, you'll likely encounter issues like this:
0.1 + 0.2; // => 0.30000000000000004
This is due to inherent mechanism by which fractions are expressed in the floating-point standard. You can imagine that if we were interested in querying whether a decimal is equal to, greater than, or less than another value, it would be as simple as using the following code:
const someValue = 0.1 + 0.2;
if (someValue === 0.3) {
yay();
}
But yay() will never run. To get around this problem, there are two options. The first involves something called the epsilon. The epsilon is the margin of error inherent to floating-point math, and JavaScript makes this available to use as Number.EPSILON:
Number.EPSILON; // => 0.0000000000000002220446049250313
This is a very tiny number, but it must be taken into account if we are to have a hope of doing basic mathematical operations on decimals. If we wish to compare two numbers, we can simply subtract them from each other and check that the margin is less than the EPSILON:
const someValue = 0.1 + 0.2;
if (Math.abs(someValue - 0.3) < Number.EPSILON) {
// someValue is (effectively) equal to 0.3
}
The other approach we can take is to convert any decimals we're dealing with into integers expressed by either Number or BigInt types. So, if we have a need to represent values from 0 to 1 with a precision of eight decimal places, for example, then we can simply multiply these values by 100,000,000 (or 108):
const unwieldyDecimalValue = 0.12345678;
// We can use 1e8 to express Math.pow(10, 8)
unwieldyDecimalValue * 1e8; // => 12345678
Now, we are free to conduct integer math on these values and divide them back down into their fractions when done. It's crucial to note that any decimal value longer than 15 digits cannot be expressed in JavaScript's Number type, so you'll need to explore other options. JavaScript currently doesn't have a native BigDecimal type, but there are many third-party libraries available that fulfill a similar purpose (you can easily find these online).
There's one more topic to discuss under the Number type, and that is NaN. NaN is a primitive that technically belongs to the Number type. It represents a failure to parse something as a number; for example, Number('wow') evaluates to NaN. Since typeof NaN is a number, we should check for a valid number in the following way:
if (typeof myNumber === 'number' && !isNaN(myNumber)) {
// Do something with your number
}
The value NaN can create a headache when its existence is not foreseen. It'll usually crop up in areas where you're attempting to cast strings to numbers or where this happens implicitly (coercion).
There is another value encapsulated by the Number type that is not a normal number: Infinity. You will receive Infinity when you attempt to do mathematical operations such as dividing by 0:
100/0; // => Infinity
Infinity, like NaN, is a globally available primitive value that you can reference and check for:
100/0 === Infinity; // => true
There is also -Infinity, which is technically a distinct value:
100/-0; // => -Infinity
-Infinity === Infinity; // => false
Infinity, like NaN, is of the Number type, so when passed to the typeof operator, it will evaluate to "number":
typeof Infinity; // => "number"
Outside of Infinity, -Infinity, and NaN, all values that are of the Number type can be considered regular everyday numbers. Overall, and for most use cases, the Number type is very simple to use and operate on. It is, however, vital to know about its limitations, many of which we've covered here so that you can make an informed decision about when it may not be appropriate to use.
The String type in JavaScript allows us to express sequences of characters. It is usually used to encapsulate words, sentences, lists, HTML, and many other forms of text-like content.
Strings are expressed by delimiting sequences of characters with either single quotes, double quotes, or backticks:
// Single quotes:
const name = 'Titanic';
// Double quotes:
const type = "Ship";
// Template literals (back-ticks):
const report = `
RMS Titanic was a British passenger liner that sank
in the North Atlantic Ocean in 1912 after the ship
struck an iceberg during her maiden voyage.
`;
Only backtick-delimited strings, known as template literals (or template strings), can occupy multiple lines. Single quote- or double quote-delimited strings can technically be spread along multiple lines as well, but this is only achieved by escaping their invisible newline characters (with a \ character), which effectively removes the newlines:
const a = "example of a \
string with escaped newline \
characters";
const b = "example of a string with escaped newline characters";
a === b; // => true
Nowadays, template literals are preferred as they retain newlines and allow us to interpolate arbitrary expressions, like so:
const nBreadLoaves = 4;
const breadLoafCost = 2.40;
`
I went to the market and bought ${nBreadLoaves} loaves of
bread and it cost me ${nBreadLoaves * breadLoafCost} euros.
`
Strings come with a number of curious challenges once your usage exceeds the most simple use cases. Under the surface, the humble string is masking a miraculous scale of complexity in the form of Unicode.
To step beneath the veneer of the String abstraction slightly, we can say that Strings in JavaScript are really just an ordered sequence of 16-bit unsigned integers. Each of these integers is interpreted as a UTF-16 code unit. UTF-16 is a type of encoding for the Unicode character set. Using it, we are able to express hundreds of thousands of valid Unicode code points. This means that we can express emojis, many languages, and a myriad of Unicode oddities via our strings:

A Unicode code point is a character (such as the letter B, a question mark, or a smiling emoji). We can express a code point by using one or more UTF-16 code units. Most code points that we use from day to day only need a single code unit. These are known as scalars. There are, however, quite a few Unicode code points that require a pair of code units (known as a surrogate pair). The panda emoji is an example of such a surrogate pair:

Since UTF-16 only has 16 bits to work with, it has to use pairs of 16-bit integers to express some characters. Naturally, if we're using UTF-32 encoding (with 32 bits to play with), then we'd be able to express the panda emoji in a single 32-bit integer.
Here, we've used charCodeAt() to determine the individual UTF-16 code units of the Panda emoji and we've found that these are the 55,357th and 56,380th decimal code units within Unicode. Since there are so many code units, it is simpler and more convenient to use hexadecimal digits to express them, so we can say that the panda emoji is expressed by code units U+D83D and U+DC3C (Unicode hexadecimal values are conventionally prefixed with U+).
In addition to surrogate pairs, there is another type of combination that's useful to know about. The Combining Code Point enables certain traditional non-combining code points to be augmented into new characters. Examples of this include traditional Latin characters that can be augmented with accents or other augmentations, such as the combining tilde:

We've chosen to express this particular combining character via a Unicode escape sequence (\u0303). The format of \uXXXX allows us to express Unicode code units between U+0000 and U+FFFF within JavaScript strings.
Our panda emoji, as we've already seen, is quite an obscure symbol. It does not exist on the BMP and is thus expressed by a surrogate pair of two UTF-16 code units. We can express these individually in JavaScript strings via two Unicode escape sequences:

More obscure and ancient symbols are found in the supplementary (or astral) planes between U+010000 and U+10FFFF. The escaping format of \uXXXX does not have enough slots for us to express these. Symbols within the astral planes require at least five hexadecimal digits to express, so we must use the more recently introduced escape sequence format of \u{X}. This provides up to six hexadecimal slots (\u{XXXXXX}) and can thus express over 1 million different code points. Using this type of escape sequence, we can express our Panda emoji directly via its 32-bit representation (U+1F43C):

The newer \u{X} escape sequence is really convenient and goes some way in making Unicode less burdensome to use than JavaScript. But there is still a little more complexity to explore. Surrogate pairs and combining characters are examples where UTF-16 code units are combined to produce individual symbols. On top of this, there are longer sequences called grapheme clusters. These are used to express combinations of code points that can be combined to create an aggregate symbol:

Wow! Unicode is a pretty incredible feat of engineering, but it can make things complicated for us. The ability to combine Unicode in all of these ways (combining characters, surrogate pairs, and grapheme clusters) creates a challenge for us. JavaScript strings, as you may know, have a length property. This property returns the number of code units in a given string (that is, the number of 16-bit integers in the entire sequence). For most strings, this is straightforward:
'fox'.length; // => 3
'12345'.length; // => 5
However, as we know, we are able to combine code units to create code points and we are also able to combine code points to create grapheme clusters. This means the length property, which is only concerned with the 16-bit code units, can give us unexpected results:

The smiling-face emoji is composed of two code units, so JavaScript correctly tells us this string has a length of 2. But this may not be what we expect or desire. It's even more challenging when we're dealing with grapheme clusters that may use a dozen different code units to express a single symbol.
Throughout this section, we've explored the tricky domain of Unicode. Va our new understanding of it, we're now far more empowered to cleanly work with strings in JavaScript. Excluding the complexity of Unicode, the behavior of strings in JavaScript is rather intuitive and shouldn't cause many headaches as long as we use them in a way that clearly communicates our intent.
The Boolean primitive type in JavaScript is used to represent either true or false. These polar opposites are its only values:
const isTrue = true;
const isFalse = false;
Semantically, Booleans are used to represent real-life or problem domain values that can be considered on or off (0 or 1), for example, whether a feature is enabled, or whether the user is over a certain age. These are Boolean characteristics and so are appropriate to express via Boolean values. We can use such values to dictate control flow within a program:
const age = 100;
const hasLivedTo100 = age >= 100;
if (hasLivedTo100) {
console.log('Congratulations on living to 100!');
}
The Boolean primitive, just like String and Number, can be manually wrapped in a wrapper instance like so:
const isTrueObj = new Boolean(true);
Note that, once you do this, Boolean will behave just like any other object in conditional statements. So, the following conditional statement will succeed, even though the wrapped primitive value is false:
const isFalseObj = new Boolean(false);
if (isFalseObj) {
// This will run
}
The Boolean instance here is not equivalent to its primitive value; it merely contains its primitive value. isFalseObj will behave just like any other Object in a Boolean context, resolving to true. Manually wrapping a Boolean like this is not especially useful and should be avoided as an anti-pattern in most programs as it doesn't behave according to Boolean semantics and may produce unexpected results.
The BigInt primitive type in JavaScript is used to represent an integer of arbitrary precision. This means that it can be used to represent integers that are not able to be precisely represented by JavaScript's Number type (anything larger than ~253). Literal BigInts are declared by suffixing any sequence of digits with the n character, like so:
100007199254740991n
BigInt is capable of representing integers of arbitrary precision, meaning that you can store integers of unlimited length. This is especially useful in financial applications or any case where high-accuracy integers need to be expressed and operated on.
A BigInt can only operate on itself, and is therefore not compatible with many of JavaScript's native Math methods:
Math.abs(1n); // !! TypeError: Cannot convert a BigInt value to a number
All native mathematics operators work with BigInt as long as both operands are of the same type:
(1n + (2n * 3n)) + 4n; // => 11n
However, if one operand is a BigInt and the other is a Number, then you'll receive a TypeError:
1n + 1; // !! TypeError: Cannot mix BigInt and other types, use explicit conversions
The semantics of a BigInt is similar to Number: any value that is intuitively numerical and can be expressed as an integer can be stored in either BigInt or Number, depending on the extent of precision that it requires.
A Symbol primitive is used to represent an entirely unique value. Symbols are created via invoking the Symbol function, like so:
const totallyUniqueKey = Symbol();
You can optionally pass an initial argument to this function to annotate your symbol for your own debugging purposes, but this is not necessary:
const totallyUniqueKey = Symbol('My Special Key');
Symbols are used to act as property keys where uniqueness is required or where you want to store metadata on objects. When you add a property to an object with a Symbol key, it will not be iterated over by normal object iteration approaches (such as for...in). Symbol keys of an object can only be retrieved via Object.getOwnPropertySymbols:
const thing = {};
thing.name = 'James';
thing.hobby = 'Kayaking';
thing[Symbol(999)] = 'Something else entirely';
for (let key in thing) console.log(key);
// => "name"
// => "hobby"
const symbols =
Object.getOwnPropertySymbols(thing); // => [Symbol(999)]
thing[symbols[0]]; // => "Something else entirely"
Since Symbol keys exist in an explicit but hidden manner, they are useful for storing programmatic information semantically that's unrelated to the core data of the object but useful in fulfilling some programmatic need. For example, you may have a logging library and wish to annotate specific objects with custom-rendering functions that log in a specific way. Such a need could be easily fulfilled with symbols:
const log = thing => {
console.log(
thing[log.CUSTOM_RENDER] ?
thing[log.CUSTOM_RENDER](thing) :
thing
);
};
log.CUSTOM_RENDER = Symbol();
class Person {
constructor(name) {
this.name = name;
this[log.CUSTOM_RENDER] = () => {
return `Person (name = ${this.name})`;
};
}
}
log(123); // => Logs "123"
log(new Person('Sarah')); // => Logs: "Person (name = Sarah)"
log(new Person('Wally')); // => Logs: "Person (name = Wally)"
log(new Person('Julie')); // => Logs: "Person (name = Julie)"
There are not many everyday situations that would necessitate the creation and usage of new symbols, but there are many instances of prescribing native behavior by such symbols. For example, you can define a custom iterator for your object by using the Symbol.iterator property. We will cover this in greater detail in the Arrays and iterables section, later in this chapter.
The null primitive type is used to express the intentional absence of a value. It is a type with only one value: the only null value is null.
The semantics of null are crucially different from undefined. The undefined value is used to indicate something that is not declared or defined, while null is an explicitly declared absent value. We usually use the null value to indicate that a value is either explicitly not yet set or, for whatever reason, unavailable.
For example, let's consider an API where we specify various properties related to a restaurant review:
setRestaurantFeatures({
hasWifi: false,
hasDisabledAccess: true,
hasParking: null
});
The null value, in this context, means that we do not know the value of hasParking yet. When we have the necessary information, we can specify hasParking as either true or false (Boolean), but to express our ignorance of its true value, we're setting it to null. We could also completely leave the value out, meaning that it would effectively be undefined. The key difference is that using null is always proactively done, while undefined is the result of wh something isn't done.
A null value, as we mentioned previously, is always falsy, meaning that it will always evaluate to false in a Boolean context. So, if we attempt to use null in a conditional statement, then it would not succeed:
function setRestaurantFeatures(features) {
if (features.hasParking) {
// This will not run as hasParking is null
}
}
It is important to check for the exact values we want so that we can avoid bugs and communicate effectively to the people reading our code. In this case, we may wish to explicitly check for undefined and null as we want to execute distinct code for that case versus the case of false. We could accomplish this like so:
if (features.hasParking !== null && features.hasParking !== undefined) {
// hasParking is available...
} else {
// hasParking is not set (undefined) or unavailable (null)
}
We can also use the abstract equality operator (==) to compare to null, which will helpfully evaluate to true if the operand is either null or undefined:
if (features.hasParking != null) {
// hasParking is available...
} else {
// hasParking is not set (undefined) or unavailable (null)
}
This is, in fact, doing the same as the more explicit comparison, but is far more succinct. Unfortunately, it's not very clear that its intention is to check for both null and undefined. We should usually prefer being explicit as this allows us to communicate our intent to other programmers in a more efficient way.
A final trap to avoid with null is the typeof operator. Due to some legacies of the JavaScript language, typeof null will, rather confusingly, return "object" and is therefore entirely unreliable.
So, there you have it. Null is a simple enough value and, insofar as clean code is concerned, you won't go wrong if you remember two key points: that it should only be used to express the intentional absence of a value and that it should, ideally, be checked explicitly (prefer value === null).
The undefined primitive type expresses that something hasn't been defined yet or remains undefined. Like null, it is a type with only one value (undefined). Unlike null, an undefined value should not be explicitly set, but may be returned by the language when something does not have a value:
const coffee = {
type: 'Flat White',
shots: 2
};
coffee.name; // => undefined
coffee.type; // => "Flat White"
Undefined is best thought of as the absence of something. If you ever find yourself wishing to explicitly set something to undefined, you should probably reach for null instead.
It's important to distinguish between the concepts of undefined and not even declared. In JavaScript, if you try to evaluate an identifier that does not exist within your scope, you will get a ReferenceError:
thisDoesNotExist; // !! ReferenceError: thisDoesNotExist is not defined
However, as you've already seen, if you try to evaluate a property of an object and the property does not exist, you will get no such error. Instead, it will evaluate to undefined:
const obj = {};
obj.foo; // => undefined
However, if you try to access a property under the non-existent foo property, you'll receive a TypeError complaining that it cannot read a property that has an undefined value:
obj.foo.baz; // !! TypeError: Cannot read property 'baz' of undefined
This behavior is an extension of the fact that seeking to access any property on an undefined or null value will always throw such a TypeError:
(undefined).foo; // !! TypeError: Cannot read property 'foo' of undefined
Curiously, the undefined value, unlike null, is not a literal, but is a globally available value provided by the language. Overwriting this global value is not possible in ECMAScript 2015 onward, but it is still possible to define your own value for the undefined identifier in local (non-global) scopes:
undefined; // => undefined
function weird() {
let undefined = 1;
undefined; // => 1
}
This is an anti-pattern as it can create very awkward and unexpected results. The accidental setting of undefined in a scope higher than your scope can mean that, if you were to rely on the value directly, you may end up referring to a value other than undefined. This lack of trust in the undefined value has historically meant that people have found other ways to forcefully make undefined available in their scope. For example, declaring a variable but not assigning it will always result in its value being undefined:
function scopeWithReliableUndefined() {
let undefined;
undefined; // => undefined
}
You can also use JavaScript's void operator on any value that will always return the real undefined value:
void 0; // => undefined
void null; // => undefined
void undefined; // => undefined
Explicitly setting undefined within your scope means that you can safely refer to your undefined value without worrying that it has been compromised. Fortunately, however, you can avoid the pain of having to worry about this risk by using the typeof operator:
if (typeof myValue === 'undefined') { ... }
This will not throw a ReferenceError even if myValue does not exist. The typeof operator, as we've discovered with null, is a bit of a fair-weather friend as we can't always rely on it, but it is nonetheless very useful when explicitly checking for undefined.
In summary, undefined can be used cleanly if you remember the following two points:
This concludes our exploration of primitive types in JavaScript. Now, we'll move on to non-primitives, that is, objects.
Everything that is not a primitive value in JavaScript can be considered an object. Even functions are, in fact, specialized objects; their only difference is that they can be invoked. Usually, however, when we use the term Object, we are referring to a plain object that is normally declared as an object literal delimited by curly braces, with a set of key-value pairs within:
const animal = {
name: 'Duck',
hobby: 'Paddling'
};
You can also instantiate an object via the Object constructor and then add properties directly:
const animal = new Object();
animal.name = 'Duck';
animal.hobby = 'Paddling';
Even though they are equivalent, it's preferable to use an object literal in most situations as it is simpler to declare and to read, especially if there are many properties. It also has the added benefit of allowing you to create and pass an object as an expression without having to prepare it beforehand.
The keys that are used to add properties to objects (the property names) are internally stored as strings. However, when using the object literal syntax, you can declare the keys as regular identifiers (that is, anything you could use as a variable name), number literals, or string literals:
const object = {
foo: 123, // Using an identifier as the key
"baz": 123, // Using a String literal as the key
123: 123 // Using a Number literal as the key
};
It's preferable to use identifiers where possible as this helpfully restricts you to using key names that can easily be accessed as properties. If you use a string literal that is not also a valid identifier, then you'll have to use square-bracket notation to access it, which can be burdensome:
const data = {
hobbies: ['tennis', 'kayaking'],
'my hobbies': ['tennis', 'kayaking']
};
data.hobbies; // Easy
data['my hobbies']; // Burdensome
You can also use computed property names (delimited by square brackets) to add dynamically named items to an object literal:
const data = {
['item' + (1 + 2)]: 'foo'
};
data; // => { item3: "foo" }
data.item3; // => "foo"
As we mentioned previously, all non-primitives in JavaScript are technically objects. What else makes something an object, though? Objects allow us to assign arbitrary values to them as properties, which is something primitives are not capable of. Beyond this characteristic, the definition of an object in JavaScript is left invitingly generic. We can wield objects in many different ways to suit the code we're writing. Many languages will provide language constructs for dictionaries or hashmaps. In JavaScript, we can use objects to fulfill most of these needs. When we need to use store a key-value pair where the key is something other than a string, it's common to provide a string representation of that value via the object's toString method:
const me = {
name: 'James',
location: 'England',
toString() {
return [this.name, this.location].join(', ')
}
};
me.toString(); // => "James, England"
String(me); // => "James, England"
This will be called internally when the object is put in a context where it is coerced to a string, such as when accessing or assigning via square-bracket notation:
const peopleInEurope = {};
peopleInEurope[me] = true;
Object.keys(peopleInEurope); // => ["James, England"]
peopleInEurope[me]; // => true
This has historically been used to allow the implementation of data structures where the key is effectively non-primitive (even though objects technically store property names as strings). Nowadays, however, using Map or WeakMap is preferred.
When adding properties to objects in the conventional fashion, either via property access or via an object literal, the properties will be given the following implicit traits:
JavaScript gives you the power to turn off these traits individually, but be wary that changes to these traits can obscure the behavior of your code. For example, if a property is described as not being writeable but a write is attempted via assignment (for example, obj.prop = 123), then the programmer will receive no warning that the write has not occurred. This can create unexpected and hard-to-find bugs. As ever, it's vital to keep in mind the expectations of the programmers who will be consuming your interfaces. So you keep the property descriptors with care and consideration.
You can define your own traits for a given property via the natively provided Object.defineProperty(). When setting up a new property descriptor, the default of each trait will be false, so if you wish to give the property a trait of either configurable, enumerable, or writable, then you will need to specify these as true explicitly:
const myObject = {};
Object.defineProperty(myObject, 'name', {
writeable: false,
configurable: false,
enumerable: true,
value: 'The Unchangeable Name'
});
myObject.name; // => "The Unchangeable Name"
myObject.name = 'something else'; // => (Ineffective)
myObject.name; // => "The Unchangeable Name"
delete myObject.name; // => false (Ineffective)
myObject.name; // => "The Unchangeable Name"
You can also use Object.defineProperties() to describe many properties at once:
const chocolate = Object.defineProperties({
// Empty object where our described properties
// will be placed
}, {
name: { value: 'Chocolate', enumerable: false },
tastes: { value: ['Bitter', 'Sweet'], enumerable: true }
});
chocolate.name; // => "Chocolate"
chocolate.tastes; // => ["Bitter", "Sweet"]
Object.keys(chocolate); // => ["tastes"]
If you attempt to change the traits of a property that has configurable set to false, then you will receive a TypeError:
const obj = {};
Object.defineProperty(
obj,
'timestamp',
{ configurable: false, value: Date.now() }
);
Object.defineProperty(
obj,
'timestamp',
{ configurable: true }
);
// ! TypeError: Cannot redefine property: timestamp
It is also possible to set custom setters and getters. A getter defines what value will be returned when a property is accessed, while a setter will define what occurs when an assignment is attempted on that property (that is, via the assignment operator). Using these can be useful in situations where you wish to have an internal implementation that holds the value in a unique way or somehow filters or processes the value upon assignment, for example:
const data = Object.defineProperties({}, {
name: {
set(name) { this.normalizedName = name.toLowerCase(); },
get() { return this.normalizedName; }
}
});
data.name = 'MoLLy BroWn';
data.name; // => "molly brown"
As the name property here has been described via defineProperties, it will have all of the default traits disabled, which means it is not enumerable, writeable, or configurable. If we try to enumerate it, we'll discover that our internally used normalizedName is found:
Object.keys(data); // => ["normalizedName"]
This is something to keep in mind when working with property descriptors. Make sure you're aware of what traits every property has and watch out for leakage of your internal implementation!
It's worth noting that it is also possible (and often preferable) to define getters and setters for properties directly within an object literal or class definition. For example, we could create a subclass of Array with the addition of a last property, which acts as a getter for the last element in the array:
class SpecialArray extends Array {
get last() { return this[this.length - 1]; }
}
const myArray = new SpecialArray('a', 'b', 'c', 'd');
myArray.last; // => "d"
myArray.push('e');
myArray.last; // => "e"
There are many such creative uses of getters and setters. But, as with the traits of configurable, enumerable, and writable, it's important to be cautious of how your custom behaviors will affect the expectations of your fellow programmers. If the abstraction or data structures you create are not familiar or predictable in their behavior, then you're paving the way for misunderstandings and bugs. The best approach is to align with the natural semantics of the language itself. So, whenever you are about to create a custom setter or describe a property as unwritable, ask yourself whether it would be reasonable for a programmer to expect it to work that way. Follow a helpful rule dubbed as the Principle of Least Astonishment (POLA)!
The Map and WeakMap abstractions are capable of storing key-value pairs where, unlike regular objects, the key can be anything, including non-primitive values:
const populationBySpecies = new Map();
const reindeer = { name: 'Reindeer', formalName: 'Rangifer tarandus' };
populationBySpecies.set(reindeer, 2000000);
populationBySpecies.get(reindeer); // => 2,000,000
WeakMap is similar to Map, but it only holds a weak reference to the object that's used as a key, meaning that, if the object becomes unavailable due to being garbage-collected elsewhere in your program, then WeakMap will cease to keep a hold of it.
Most of the time, a plain object is all you will need. You should only reach for Map or WeakMap if you need your keys to be non-primitive or if you want to weakly hold your values.
JavaScript is a prototypical language where inheritance is achieved via prototypes. This can be a daunting concept, but it is, in fact, beautifully simple. JavaScript's prototypal behavior can be described like this: every time a property is accessed on an object, if it is not available on the object itself, JavaScript will attempt to access it on an internally available property called [[Prototype]]. It will then repeat this process until it either finds the property or gets to the top of the prototype chain and returns undefined.
A [[Prototype]] object, which could feasibly be attached to any other object, is just a regular object itself. We could create one called engineerPrototype and have it contain data and methods related to the role of an engineer, for example:
const engineerPrototype = {
type: 'Engineer',
sayHello() {
return `Hello, I'm ${this.name} and I'm an ${this.type}`;
}
};
Then, we could attach this prototype to another object, thus making its properties available there as well. To do this, we use Object.create(), which creates a new object with a hardcoded [[Prototype]]:
const pandaTheEngineer = Object.create(engineerPrototype);
With this newly created pandaTheEngineer object, we are able to access any properties available on its [[Prototype]], such as engineerPrototype:
pandaTheEngineer.name = 'Panda';
pandaTheEngineer.sayHello(); // => "Hello, I'm Panda and I'm an Engineer"
We can illustrate that the objects are now linked by adding a new property to engineerPrototype and observe how it is made available on pandaTheEngineer:
pandaTheEngineer.sayGoodbye; // => TypeError: sayGoodbye is not a function
engineerPrototype.sayGoodbye = () => 'Goodbye!';
pandaTheEngineer.sayGoodbye(); // => 'Goodbye!'
As we mentioned previously, the [[Prototype]] of an object will only be used to resolve a property if it is not already available on the object itself. The following code shows how we can set our own sayHello method on our pandaTheEngineer object, and that by doing so we no longer have access to the sayHello method defined on [[Prototype]]:
pandaTheEngineer.sayHello = () => 'Yo!';
pandaTheEngineer.sayHello(); // => "Yo!"
However, deleting this newly added sayHello method would mean we once again have access to the [[Prototype]] sayHello method:
delete pandaTheEngineer.sayHello;
pandaTheEngineer.sayHello(); // => // => "Hello, I'm Panda and I'm an Engineer"
To understand what's happening and which properties are coming from which object, we are always able to inspect the [[Prototype]] of an object using Object.getPrototypeOf:
// We can inspect its prototype:
Object.getPrototypeOf(pandaTheEngineer) === engineerPrototype; // => true
Now, we can inspect its properties via Object.getOwnPropertyNames:
Object.getOwnPropertyNames(
Object.getPrototypeOf(pandaTheEngineer)
); // => ["type", "sayHello", "sayGoodbye"]
Here, we can see that the [[Prototype]] object (that is, engineerPrototype) is providing the type, sayHello, and sayGoodbye properties. If we inspect the pandaTheEngineer object itself, we can see that it only has a name property:
Object.getOwnPropertyNames(pandaTheEngineer); // => ["name"]
As we observed with our earlier addition of the sayGoodbye method, we can modify that prototype at any time and have our changes accessible to any objects that use that prototype. Here's another example of doing this:
// Modify the prototype object:
engineerPrototype.type = "Awesome Engineer";
// Call a method on our object (that uses the prototype):
pandaTheEngineer.sayHello(); // => "Hello, I'm Panda and I'm an Awesome Engineer"
Here, you can see how our inherited sayHello method is producing a string that includes our mutated type property (that is, "Awesome Engineer").
We could feasibly create another prototype that itself uses engineerPrototype, possibly fullStackEngineerPrototype, and it would work as expected, with each prototype defining another layer of property resolution.
Below the surface, JavaScript's newer Class Definition Syntax, which you may have grown accustomed to, relies on this underlying mechanism of prototypes as well. This can be observed here:
class Engineer {
type = 'Engineer'
constructor(name) {
this.name = name;
}
sayHello() {
return `Hello, I'm ${this.name} and I'm an ${this.type}`;
}
}
const pandaTheEngineer = new Engineer();
Object.getOwnPropertyNames(pandaTheEngineer); // => ["type", "name"]
Object.getOwnPropertyNames(
Object.getPrototypeOf(pandaTheEngineer)
); // => ["constructor", "sayHello"]
You'll notice that there are some subtle differences here. The most crucial one is that, when declaring classes, there is currently no way to define non-method properties on the prototype object. When we declare the type property, we are populating the instance itself so that when we inspect the properties of the instance, we get "type" and "name". However, the methods (such as sayHello) will exist on the [[Prototype]]. Another difference is that, of course, when using classes, we are able to declare a constructor, which itself is a method/property on the [[Prototype]].
Before class definitions existed, we typically wrote class-like abstractions by assigning our intended [[Prototype]] object to the prototype property of a function, as shown here:
function Engineer(name) {
this.name = name;
}
Engineer.prototype = {
type: 'Engineer',
sayHello() {
return `Hello, I'm ${this.name} and I'm an ${this.type}`;
}
};
When a function is instantiated via the new operator, JavaScript will implicitly create a new object with its [[Prototype]] set to the function's prototype property, if it has one. Let's try instantiating the Engineer function:
const pandaTheEngineer = new Engineer();
Inspecting this yields the same characteristics that we saw in our original Object.create approach:
Object.getOwnPropertyNames(pandaTheEngineer); // => ["name"]
Object.getOwnPropertyNames(
Object.getPrototypeOf(pandaTheEngineer)
); // => ["type", "sayHello"]
Broadly, all of these approaches are the same but have some subtle differences around where certain properties reside (that is, whether its properties are on the instance itself or on its [[Prototype]]). The newer Class Definition Syntax is useful and succinct and so is preferable nowadays, but it is nonetheless useful to have an underlying knowledge about how prototypes work as it drives the entirety of the language, including all of its native types. We can inspect these native types in the same manner as in the preceding code:
const array = ['wow', 'an', 'array'];
Object.getOwnPropertyNames(array); // => ["0", "1", "2", "length"]
Object.getOwnPropertyNames(
Object.getPrototypeOf(array)
); // => ["constructor", "concat", "find", "findIndex", "lastIndexOf", "pop", "push", ...]
If you ever catch yourself trying to modify a native prototype, it may be better if you created your own subclass of that type and added your functionality there:
class HeartArray extends Array {
join() {
return super.join(' ❤ ');
}
}
const yay = new HeartArray('this', 'is', 'lovely');
yay.join(); // => "this ❤ is ❤ lovely"
Here, we're creating our own Array subclass called HeartArray so that we can add our own specialized join method.
An object of any type, much like our primitive values, should only be used inline with the semantic concept it represents. The preceding case of subclassing Array to HeartArray makes sense as the data we wish to express via it is indeed array-like, that is, it is a sequential set of words.
When we go about molding objects into abstractions that suit our needs, we should always consider the expectations that other programmers will have about objects and the ramifications of those expectations. We'll go into the subtleties of designing good abstractions in Chapter 11, Design Patterns, where we will be utilizing objects to craft abstractions in a multitude of ways.
This section has introduced you to the concept of objects in JavaScript—how they are everywhere—and how they operate beneath the surface via prototypes. This fundamental knowledge will make working with JavaScript much easier and will help you write cleaner code.
Functions in JavaScript are like any other type; they can be passed around just like objects and primitive types. When we talk about most other values, however, we see that there is usually only one way to literally declare them. Object literals are declared using braces. Array literals are delimited square brackets. Functions, however, come in a variety of literal forms.
When outside of an object literal or class definition, you can declare a function in three different ways: as a function declaration, as a function expression, or as a fat-arrow function expression:
// Function Declaration
function myFunction() {}
// Function Expression
const myFunction = function () {};
// Named Function Expression
const myFunction = function myFunction() {};
// "Fat"-Arrow Function Expression
const myFunction = () => {};
When declaring functions inside object literals, however, there is a more succinct syntax, called a method definition:
const things = {
myMethod() {},
anotherMethod() {}
};
We need to separate these method definitions with a comma (just as we'd have to do with any other properties defined in an object literal). Class definitions also allow us to use method definitions, although they don't require separating commas:
class Thing {
myMethod() {}
anotherMethod() {}
}
All the various ways of defining functions have subtle differences that are worth knowing about because the typical JavaScript code base will use most, if not all, of these styles. The types of differences you'll encounter in how functions are declared include the following:
Now, we can go into more detail about the syntax of the various definition styles.
There are three syntactic contexts in which a function can exist:
The difference between functions as statements and functions as expressions is exemplified by the function expression and the function declaration. The function declaration is quite unique in that it is the only way to declare a function that is technically a statement. To be considered a function declaration, the syntax of function name() {} must reside on its own without being used in the context of an expression. This can be incredibly confusing because you cannot always tell whether a function is a function declaration or function expression based purely on its own syntax; instead, you must look at the context in which it exists:
// This is a statement, and a function declaration:
// And will therefore be hoisted:
function wow() {}
// This is a statement containing a function expression:
const wow = function wow() {};
As we mentioned previously, a function expression is allowed to have a name, just like a function declaration, but that name may not match the name of the variable that the function is assigned to.
It's easiest to think of expressions as anything that can legally exist to the right-hand side of the assignment operator. All of the following right-hand sides are legal expressions:
foo = 123;
foo = [1,2,3];
foo = {1:2,3:4};
foo = 1 | 2 | 3;
foo = function() {};
foo = (function(){})();
foo = [function(){}, ()=>{}, function baz(){}];
Function expressions are as flexible as all the other values in JavaScript in terms of where they can be placed syntactically. Function declarations, as we will discover, are limited. Method definitions are also limited to exist within the confines of either an object literal or a class definition.
A function's bindings refer to a set of additional and implicit values that JavaScript makes available for referencing within the body of the function. These bindings include the following:
These bindings are available to all functions except those defined with the arrow syntax (fn = () => {}). Functions defined in this way will effectively absorb the bindings from the parent scope (if one is available). Each of these bindings has unique behaviors and constraints. We will explore these in the following subsections.
The this keyword is usually determined at the calltime of the function and will normally resolve to the object that the function is being invoked on. It is sometimes referred to as the execution context of a function or its thisArg. This can be unintuitive since it means that the this value can technically change between calls. For example, we could assign a method from one object to another, call it on the second, and observe that its this is always the object it's been called on:
const london = { name: 'London' };
const tokyo = { name: 'Tokyo' };
function sayMyName() {
console.log(`My name is ${this.name}`);
}
sayMyName(); // => Logs: "My name is undefined"
london.sayMyName = sayMyName;
london.sayMyName(); // => Logs "My name is London"
tokyo.sayMyName = sayMyName;
tokyo.sayMyName(); // => Logs "My name is Tokyo"
When called without an object of invocation, as is the case when we directly call sayMyName, its presumed execution context is the global environment in which the code resides. On the browser, this global environment is equal to the window object (which provides access to the browser-and-document object models), while in Node.js, this refers to an environment that's unique to each specific module/file, which includes, among other things, that module's exports.
In addition to the case of calling a function globally, there are two cases where the this keyword will be something other than the apparent object of invocation:
There are also ways to force the value of this when calling or declaring a function. You can use bind(X) to create a new function that will have its this value set to X:
const sayHelloToTokyo = sayMyName.bind(tokyo);
sayHelloToTokyo(); // => Logs "My name is Tokyo"
You can also use a function's call and apply methods to force the this value for any given invocation, but note that this will not work if the function is being called as a constructor (that is, with a new keyword) or if it has been defined with the arrow-function syntax:
// Forcing the value of `this` via `.call()`:
tokyo.sayMyName.call(london); // => Logs "My name is London"
In your everyday function calls, it's best to avoid awkward techniques of invocation like this. Such techniques can make it difficult for the readers of your code to discern what's happening. There are many valid applications of invoking via call, apply, or bind, but these are usually limited to lower-level libraries or utility code. Higher-level logic should avoid them. If you find yourself having to rely on these methods in higher-level abstractions, then you're likely making something more complicated than it needs to be.
The super keyword comes in three distinct flavors:
The super keyword was introduced to the language at the same time as the class definition and method definition syntax, so it is tied up in those constructs. You are free to use super in class constructors, methods, and also in method definitions within object literals:
const Utils {
constructor() {
super(); // <= I can use super here
}
method() {
super.method(); // <= And here...
}
}
const utils = {
method() {
return super.property; // <= And even here...
}
};
The super keyword, as its name suggests, is semantically suited to referencing a superclass, so 99% of its valid use cases will be within class definitions, where you're seeking to reference the class being extended, like so:
const Banana extends Fruit {
constructor() {
super(); // Call the Fruit constructor
}
}
Using super in this manner is entirely intuitive, especially to programmers who are used to other OOP languages. For individuals adept with JavaScript's prototype mechanism, however, the implementation of super can seem confusing. Unlike the this value, super is bound at definition time, instead of call time. We've seen how we can manipulate the value of this by calling a method in a specific manner (for example, using fn.call()). You cannot similarly manipulate super. Hopefully, this will not affect you in any way, but it is useful to remember nonetheless.
The new.target binding will be equal to the current function being called if the function has been called via a new operator. We typically use the new operator to instantiate classes, and in this case, we will correctly expect new.target to be that class:
class Foo {
constructor() {
console.log(new.target === Foo);
}
}
new Foo(); // => Logs: true
This is useful when we may wish to carry out a certain behavior if a constructor is called directly versus when called via new. A common defensive strategy is to make your constructor behave in the same way, regardless of whether it's called with or without new. This can be achieved by checking for new.target:
function Foo() {
if (new.target !== Foo) {
return new Foo();
}
}
new Foo() instanceof Foo; // => true
Foo() instanceof Foo; // => true
Alternatively, you may wish to throw an error to check that a constructor has been invoked incorrectly:
function Foo() {
if (new.target !== Foo) {
throw new Error('Foo is a constructor: please instantiate via new Foo()');
}
}
Foo() instanceof Foo; // !! Error: Foo is a constructor: please instantiate via new Foo()
Both of these examples would be considered intuitive use cases of new.target. There is, of course, the possibility to use it to deliver entirely different functionality depending on the calling pattern, but in the interest of catering to the reasonable expectations of programmers, it's best to avoid such behavior. Remember the POLA.
The arguments binding is made available as an array-like object and will contain the arguments that a given function was called with.
Here, we can observe that the arguments are provided within the scope of a given function:
function sum() {
arguments; // => [1, 2, 3, 4, 5] (Array-like object)
let total = 0;
for (let n of arguments) total += n;
return total;
}
sum(1, 2, 3, 4, 5);
The arguments binding was popularly used to gain access to an arbitrary (that is, non-fixed) amount of arguments, though its usefulness quickly disappeared after the language introduced the rest parameter syntax (...arg). This newer syntax can be used when defining a function to instruct JavaScript to place remaining arguments into a singular array. This meant that you could achieve all of the utility of the older arguments binding, plus you'd have a value that was not merely array-like but actually a genuine array. Here's an example:
function sum(...numbers) {
// We can call reduce() on our array:
return numbers.reduce((total, n) => total + n, 0);
}
sum(1, 2, 3, 4, 5);
Even though the arguments object has fallen out of favor, it is still within the language spec and works in older environments, so you may still see it in the wild. Most of the time, its usage can be avoided.
Confusingly, functions have names, and these names are not the same as the variables or properties that we assign to functions. The name of a function is within its syntax, prior to its parentheses:
function nameOfTheFunction() {}
You can access a function's name via its name property:
nameOfTheFunction.name; // => "nameOfTheFunction"
When you define a function via the function declaration syntax, it'll assign that function to a local variable of the same name, meaning that we can reference the function as we would expect:
function nameOfTheFunction() {}
nameOfTheFunction; // => the function
nameOfTheFunction.name; // => "nameOfTheFunction"
Method definitions will also assign the method to a property name that is equal to the function name:
function nameOfTheFunction() {}
nameOfTheFunction; // => the function
nameOfTheFunction.name; // => "nameOfTheFunction"
You may be thinking this all seems incredibly intuitive. And it is. It makes perfect sense that the names we give our functions and methods are themselves used to dictate what variable or property those things will be assigned to. Oddly, though, it is also possible to have named function expressions, and these names do not cause such an assignment. The following is an example of this:
const myFunction = function hullaballoo() {}
The const name here, myFunction, dictates what we will use in subsequent lines to reference the function. However, the function technically has a name of "hullaballoo":
myFunction; // => the function
myFunction.name; // => "hullaballoo"
If we try to reference the function via its formal name, we will get an error:
hullaballoo; // !! ReferenceError: hullaballoo is not defined
This can seem odd. Why is it possible to give a function a name if that name itself is not used to refer to the function? This is a mixture of legacy and convenience. One hidden feature of the named function expression is that the name is actually available to you to reference the function, but only inside the scope of the function itself:
const myFunction = function hullaballoo() {
hullaballoo; // => the function
};
This can be useful in situations where you want to supply an anonymous callback to some other function but still be able to reference your own callback for any repeated or recursive calls, like so:
[
['chris', 'smith'],
['sarah', ['talob', 'peters']],
['pam', 'taylor']
].map(function capitalizeNames(item) {
return Array.isArray(item) ?
item.map(capitalizeNames) :
item.slice(0, 1).toUpperCase() + item.slice(1);
});
// => [["Chris","Smith"],["Sarah",["Talob", "Peters"]],["Pam","Taylor"]]
So, even though the named function expression is an odd thing, it does have its merits. In usage, however, it's best to take into consideration the clarity of your code for people who might not know of these idiosyncratic behaviors. This does not mean avoiding it altogether, but just being ever more mindful of the readability of your code when using it.
Function declarations are a type of hoisted declaration. A hoisted declaration is one that will, at runtime, be effectively hoisted up the top of its execution context, meaning that it will be immediately accessible to preceding lines of code (seemingly before it's declared):
hoistedDeclaration(); // => Does not throw an error...
function hoistedDeclaration() {}
This, of course, is not possible with a function expression that's been assigned to a variable:
regularFunctionExpression();
// => Uncaught ReferenceError:
// => Cannot access 'regularFunctionExpression' before initialization
const regularFunctionExpression = function() {};
The hoisted behavior of function declarations can create unexpected results, so it is typically considered an anti-pattern to rely on the hoist. In general, it's fine to use function declarations, as long as they're used in a way that respects the assumptions that programmers will intuitively make. Hoisting, as a practice, is not very intuitive to most people, and so it's usually best to avoid it.
Function expressions are the easiest and most predictable to use as they are syntactically similar to all the other values within JavaScript. You can use them to literally define functions anywhere you would define any other value as they are a type of expression. Observe here, for example, how we're defining an array of functions:
const arrayOfFunctions = [
function(){},
function(){}
];
A common application of the function expression is in passing callbacks to other functions so that they can be called at some later point. Many native Array methods, such as forEach, accept functions in this manner:
[1, 2, 3].forEach(function(value) {
// do something with each value
});
Here, we are passing a function expression to the forEach method. We haven't named this function by assigning it to a variable, so it is considered an anonymous function. Anonymous functions are useful as they mean that we don't need to preassign a function to a variable in order to make use of it; we can simply write our function into our code at the exact location of usage.
The function expression is most similar in its expressive manner to the arrow function. The key difference, as we will discover, is that the arrow function does not have access to its own bindings (for example, to this or arguments). A function expression, however, does have access to these values, and so in some contexts will be more useful to you. It's very common to need a binding to this in order to operate successfully with the DOM API, for example, where many native DOM methods will invoke callbacks and event handlers with the relevant element as execution context. Additionally, you'll want to use function expressions when defining methods on objects or prototypes that will need to access the current instance. As illustrated here, using an arrow function would not be appropriate:
class FooBear {
name = 'Foo Bear';
}
FooBear.prototype.sayHello = () => `Hello I am ${this.name}`;
new FooBear().sayHello(); // => "Hello I am ";
FooBear.prototype.sayHello = function() {
return `Hello I am ${this.name}`;
};
new FooBear().sayHello(); // => "Hello I am Foo Bear";
As you can see, using the arrow function syntax prevents us from accessing the instance via this, while the function expression syntax allows us to do this. Therefore, the function expression, although somewhat superseded by the more succinct arrow function, is still a very useful tool.
The arrow function is, in many ways, just a slightly more succinct version of the function expression, although it does have some practical differences. It comes in two flavors:
// Regular Arrow Function
const arrow = (arg1, arg2) => { return 123; };
// Concise Arrow Function
const arrow = (arg1, arg2) => 123;
As you can see, the concise variant includes an implicit return, while the regular variant, much like other function definition styles, requires you to define a regular function body delimited by curly braces in which you must explicitly return a value with a return statement.
Additionally, arrow functions allow you to avoid using parentheses when declaring a function with only one argument. In these cases, you can just place the identifier of the argument by itself prior to the arrow, like so:
const addOne = n => n + 1;
The succinctness of the arrow function can be very useful in situations where you need to pass functions around quite a lot. This is common, for example, when operating on arrays via native methods such as map:
[1, 2, 3]
.map(n => n*2)
.map(n => `Number ${n}`);
// => ["Number 2", "Number 4", "Number 6"]
Despite its superhero status as the succinct variant of otherwise verbose function definitions, the arrow function comes with its own challenges. The fact that the language must accommodate both concise and regular variants of syntax means that there is some ambiguity when attempting to return an object literal from the concise arrow function:
const giveMeAnObjectPlease = () => { name: 'Gandalf', age: 2019 };
// !! Uncaught SyntaxError: Unexpected token `:`
This syntax will confuse the JavaScript parser as the opening curly brace implies that a regular function body resides within. Due to this, the parser gives us an error about an unexpected token as it is not expecting the body of an object literal. If we want to return an object literal from the concise form of the arrow function, then we must awkwardly wrap it in parentheses to disambiguate the syntax:
const giveMeAnObjectPlease = () => ({ name: 'Gandalf', age: 2019 });
Functionally, the arrow function differs from the function expression in two ways:
These differences mean that, on the whole, arrow functions are typically unsuitable to use as methods or constructors. They are best used in contexts where you wish to pass a callback or handler to another function, and especially in cases where you wish to retain your this binding. For example, if we were to bind event handlers within the context of a UIComponent abstraction, we may wish to retain the this value in order to carry out certain instance-specific functionality:
class MyUIComponent extends UIComponent {
constructor() {
this.bindEvents({
onClick: () => {
this; // <= usefully refers to the MyUIComponent instance
}
});
}
}
The arrow function feels most at home in scenarios like this. Its succinctness, however, means that there can be a risk of confusion when reading overly dense lines of code, such as the following:
process(
n=>n.filter((nCallback, compute)=>compute(()=>nCallback())
)
For this reason, it is best to employ the arrow function with the same consideration and practicality with which you would employ any other construct: ensure that you always put the usability and readability of your code first, above the very enticing niftiness of cool or succinct syntax.
Function expressions and arrow functions are the only function definition styles that are, technically, expressions. As we have seen, this quality makes them useful when we need to pass them as values to other functions without having to go through the process of assignment.
As we mentioned previously, a function without an assignment, and thus without a reference to its value, is typically called an anonymous function and will look like this:
(function() {
// I am an anonymous function
})
The idea of an anonymous function is extended further by the concept of an Immediately Invoked Function Expression (IIFE). An IIFE is just a regular anonymous function that is invoked immediately, like so:
(function() {
// I am immediately invoked
}());
Note the invocation parentheses (that is, ...()) after the closing curly brace. This will call the function and thus makes the preceding syntactic construct an IIEE.
An IIFE is not a distinct concept within the language itself. It is just a useful term that the community has come up with to describe the common pattern of immediately invoking a function. It's a useful pattern because it allows us to create an ad hoc scope, meaning that any variables defined within it are constrained to that scope and will not leak outside, just as we'd expect from any function. This immediate scope is useful to quickly do self-contained work without affecting the parent scope.
The exact syntax of an IIFE can vary. For example, if we use an arrow function, then the calling parenthesis must be placed after the wrapped function expression:
(() => {
// I am immediately invoked
})(); // <- () actually calls the function
The mechanism remains essentially the same, regardless of whether we use a function expression or an arrow function.
If the concept of an IIFE is confusing, it's simpler to understand what's going on if we replace the actual function with an identifier, fn, and imagine that we have previously assigned a function to this identifier. Here, we can call fn like so:
fn();
Now, we could choose to wrap the fn reference in parentheses. This would make no difference to the invocation, although it may look bizarre:
(fn)();
It's useful to remember that parentheses are just syntactic vessels that are sometimes needed to avoid syntactic ambiguity. So, all of these are technically equivalent:
fn();
(fn)();
((fn))();
If we replace the fn reference here with an inline anonymous function, nothing groundbreaking occurs. Instead of referencing an existing function, we are just expressing an inline function, on the spot, and then invoking it:
(function() {
// Called immediately...
})();
IIFEs are useful because they provide scope isolation without the burden of having to define a function with a name and then later reference and invoke it, as we're doing here:
const initializeApp = () => {
// Initializing...
};
initializeApp();
Within the browser, prior to complex builds involving compilation and bundling, IIFEs were useful because they provided scope isolation while not leaking any names into the global scope. Nowadays, however, the IIFE is rarely necessary.
Interestingly, the initializeApp function in the preceding code is, arguably, more readable and understandable with an explicit name. This is why, even if necessary, IIFEs are sometimes considered needlessly confusing and fancy. A named function usefully provides a clue as to its purpose and the intent of the author. Without a name, the reader of our code is left with the cognitive burden of having to read through the function itself to discover its broad purpose. For this reason, it is usually preferable to avoid IIFEs and similar anonymous constructs unless you have a very specific need.
Method definitions were added to the language at the same time as class definitions to allow you to easily declare methods bound to a specific object. They are not limited to class definitions, though. You can use them freely in object literals as well:
const things = {
myFunction() {
// ...
}
};
In classes, you can also declare methods in this manner:
class Things {
myFunction() {
// ...
}
}
You can also use traditional styles of function definition to declare your methods, such as a function expression assigned to an identifier:
class Things {
myFunction = function() {
// ...
};
}
There is, however, a crucial difference between method definitions and other styles of function definition. A method definition will always be bound to the object in which it was first defined. This is known internally as its [[HomeObject]]. This home object will determine what super binding is available to the method when it is called. Only method definitions are allowed to make reference to super, and the super they reference will always be the [[Prototype]] of their [[HomeObject]]. This means that if you try to borrow methods from other objects, you may be surprised to discover that super is not what you intended:
class Dog {
greet() { return 'Bark!'; }
}
class Cat {
greet() { return 'Meow!'; }
}
class JessieTheDog extends Dog {
greet() { return `${super.greet()} I am Jessie!`; }
}
class JessieTheCat extends Cat {
greet() { return `${super.greet()} I am Jessie!`; }
}
Here, we can observe that both JessieTheCat and JessieTheDog have greet methods:
new JessieTheDog().greet(); // => "Bark! I am Jessie!"
new JessieTheCat().greet(); // => "Meow! I am Jessie!"
We can also observe that their greet methods are implemented in identical ways. They both return the interpolated string `${super.greet()} I am Jessie!`. Due to this, it might seem logical to be able to let JessieTheCat borrow the method from JessieTheDog. After all, they're exactly the same:
class JessieTheCat extends Cat {
greet = JessieTheDog.prototype.greet
}
We might intuitively expect super in the greet method to refer to the superclass of the current instance, which in the case of JessieTheCat will be Cat. But curiously, when we call this borrowed method, we experience something different:
new JessieTheCat().greet(); // => "Bark! I am Jessie!"
It barks! The borrowed method has annoyingly retained its binding to its original [[HomeObject]].
In summary, method definitions are simpler and more succinct variants of their more verbose cousins, the function declaration and function expression. However, they come with an implicit mechanic that sets them apart and can create confusion. 99% of the time, method definitions won't bite you; they'll behave as expected. The other 1% of the time, it's useful at the very least to know why your code is misbehaving so that you can explore other options. As always, knowledge about the idiosyncrasies of JavaScript can only help us in our pursuit of a cleaner and more reliable code base.
Asynchronous (async) functions are specified with an async keyword preceding the function keyword. All function definition styles can be prefixed with it:
// Async Function Declaration:
async function foo() {}
// Async Function Expression:
const foo = async function() {};
// Async Arrow-Function:
const foo = async () => {};
// Async Method Definition:
const obj = {
async foo() {}
};
An async function allows you to easily conduct asynchronous operations by providing you with two key features:
The Promise is a natively supplied abstraction for dealing with asynchronous operations. It can seem complicated, but it's best to think of a Promise as an object that will either resolve or reject at a time later than now (that is, asynchronously).
Traditionally, in JavaScript, we'd have to pass around callbacks to ensure that we're able to respond to such asynchronous activity:
getUserDetails('user1', function(userDetails) {
// This callback is called asynchronously
});
However, with an async function and await, we can achieve this more succinctly:
const userDetails = await getUserDetails('user1');
The await clause here will halt current execution until getUserDetails completes and resolves to a value. Note that we can only use await within functions that are themselves async.
Other than allowing await clauses and returning Promises, async functions carry the same features and characteristics as the respective function definition style that's used. An async arrow function, just like a regular arrow function, does not have its own bindings to this or arguments. An async function declaration is hoisted just like its non-async cousin. Essentially, async should be thought of as a layer atop all of the learnings you've already picked up concerning the different function definition styles.
The very last type of function definition style we will cover is the very powerful generator function. Broadly, generators are used to supply and control the iteration behavior for a sequence of one or more, or even infinite, items.
Generator functions in JavaScript are specified with an asterisk following the function keyword:
function* myGenerator() {...}
When called, they will return a generator object, which uniquely conforms to both the iterable protocol and the iterator protocol, meaning that they can be iterated over themselves or can serve as an object's iteration logic.
A generator function will halt and return a value at the point of a yield statement, and this can occur multiple times. After a yield, the function is effectively stalled while it waits for a consumer to need its next value. This is best illustrated with an example:
function* threeLittlePiggies() {
yield 'This little piggy went to market.';
yield 'This little piggy stayed home.';
yield 'This little piggy had roast beef.';
}
const piggies = threeLittlePiggies();
piggies.next().value; // => 'This little piggy went to market.'
piggies.next().value; // => 'This little piggy stayed home.'
piggies.next().value; // => 'This little piggy had roast beef.'
piggies.next(); // => {value: undefined, done: true}
As you can see, the generator object that's returned from the function has a next method, which, when called, will return an object with a value (indicating the current value of the iteration) and a done property (indicating whether the iteration/generation is complete). This is the iterator protocol and is the contract you can expect all generators to fulfill.
A generator fulfills not only the iterator protocol but also the iterable protocol, which means it can be iterated over by language constructs that accept iterables (such as for...of or the ...spread operator):
for (let piggy of threeLittlePiggies()) console.log(piggy);
// => Logs: "This little piggy went to market."
// => Logs: This little piggy stayed home."
// => Logs: This little piggy had roast beef."
[...threeLittlePiggies()];
// => ["This little piggy went to market", "This little piggy stayed...", "..."]
Async generator functions can also be specified. They usefully combine the async and generator formats into a hybrid that allows for custom asynchronous generation logic, like so:
async function* pages(n) {
for (let i = 1; i <= n; i++) {
yield fetch(`/page/${i}`);
}
};
// Fetch five pages (/page/1, /page/2, /page/3)
for await (let page of pages(3)) {
page; // => Each of the 3 pages
};
You'll notice how we're using the for await iteration construct to iterate through our asynchronous generator. This will ensure that each iteration will await its result before continuing.
Generator functions are very powerful, but it's important to be aware of the underlying mechanics at play. They are not regular functions and are not guaranteed to run to completion. Their implementation should take into account the context in which they will be run. If your generator is intended to be used as an iterator, then it should respect the implied expectations of iteration: that it is a read-only operation of an underlying piece of data or generation logic. While it is possible to mutate underlying data within a generator, this should be avoided.
An array in JavaScript is a type of object that is specialized in that it contains a set of ordered elements.
You can express an array using its literal syntax, which is a comma-separated list of expressions delimited by square brackets:
const friends = ['Rachel', 'Monica', 'Ross', 'Joe', 'Phoebe', 'Chandler'];
These comma-separated expressions can be as complex or simple as we desire:
[
[1, 2, 3],
function() {},
Symbol(),
{
title: 'wow',
foo: function() {}
}
]
An array is capable of containing all manner of values. There are very few constraints on how we can use arrays. Technically, an array's limited to a length of around 4 billion, due to its length being stored as a 32-bit integer. For most purposes, of course, this should be absolutely fine.
Arrays have a numeric property for every indexed element within them and a length property to describe how many elements there are. They also have a set of useful methods for reading from and operating on the data within them:
friends[0]; // => "Rachel"
friends[5]; // => "Chandler"
friends.length; // => 6
friends.map(name => name.toUpperCase());
// => ["RACHEL", "MONICA", "ROSS", "JOE", "PHOEBE", "CHANDLER"]
friends.join(' and ');
// => "Rachel and Monica and Ross and Joe and Phoebe and Chandler"
Historically, arrays were iterated over using conventional for(...) and while(...) loops that increment a counter toward the length so that, upon each iteration, the current element could be accessed via array[counter], like so:
for (let i = 0; i < friends.length; i++) {
// Do something with `friends[i]`
}
Nowadays, however, it's preferable to use other methods of iteration, such as forEach or for...of:
for (let friend of friends) {
// Do something with `friend`
}
friends.forEach((friend, index) => {
// Do something with `friend`
});
for...of has the benefit of being breakable, meaning you can use break and continue statements within them and easily escape from the iteration. It will also work on any object that is iterable, whereas forEach is only an Array method. The forEach style, however, is useful in that it provides you with the current index of the iteration via the second argument to your callback.
Most native Array methods are generic, meaning that they can be used on any object that looks like an array. All we need to achieve the appearance of an array is use a length property and individual properties for each index (indexed from zero):
const arrayLikeThing = {
length: 3,
0: 'Suspiciously',
1: 'similar to',
2: 'an array...'
};
// We can "borrow" an array's join method by assigning
// it to our object:
arrayLikeThing.join = [].join;
arrayLikeThing.join(' ');
// => "Suspiciously similar to an array..."
Here, we've constructed an array-like object and then provided it with a join method of its own by borrowing the join method of an array (that is, from Array.prototype). The native array join method is so generically implemented that it doesn't mind operating on an object as long as that object fulfills the contract of an array by providing a length property and corresponding indexes (0, 1, 2, and so on). Most native array methods are similarly generic.
One example of an array-like object within the language itself is the arguments binding that we explored earlier in this chapter. Another example is NodeList, which is a type of object that is returned from various DOM selection methods. If necessary, we can derive proper arrays from these objects by borrowing and calling the array slice method, like so:
const arrayLikeObject = { length: 2, 0: 'foo', 1: 'bar' };
// "Borrowing" a method from an array and forcing its
// execution context via call():
[].slice.call(arrayLikeObject);
// "Borrowing" a method explicitly from the Array.prototype
// and forcing its execution context via call():
Array.prototype.slice.call(arrayLikeObject);
However, in the case of arguments or the NodeList object, we can also rely on them being iterable, meaning that we can use the spread syntax to derive a true array:
// "spread" a NodeList into an Array:
[...document.querySelectorAll('div span a')];
// "spread" an arguments object into an Array:
[...arguments];
If you find yourself needing to create an array-like object, consider having it implement the iterable protocol (which we're about to explore) so that the spread syntax can be used in this way.
Set and WeakSet are native abstractions that allow us to store sequences of unique objects. This is in contrast to arrays, which give you no assurances as to the uniqueness of your values. Here's an illustration:
const foundNumbersArray = [1, 2, 3, 4, 3, 2, 1];
const foundNumbersSet = new Set([1, 2, 3, 4, 3, 2, 1]);
foundNumbersArray; // => [1, 2, 3, 4, 3, 2, 1]
foundNumbersSet; // => Set{ 1, 2, 3, 4 }
As you can see, values given to a Set will always be ignored if they already exist in the Set.
Sets can be initialized by passing an iterable value to the constructor; for example, a string:
new Set('wooooow'); // => Set{ 'w', 'o' }
If you need to convert a Set into an array, you can most simply do this with the spread syntax (as sets are, themselves, iterable):
[...foundNumbersSet]; // => [1, 2, 3, 4]
WeakSets are similar to the previously covered WeakMaps. They are for weakly holding values in a way that allows that value to be garbage-collected in another part of the program. The semantics and best practices around using sets are similar to those concerning arrays. It's advisable to only use sets if you need to store unique sequences of values; otherwise, just use a simple array.
The iterable protocol allows values containing sequences to share a common set of characteristics, allowing them to all be iterated over or treated in a similar way.
Any object can define its own iterable protocol by simply supplying an iterator function under the property name's Symbol.iterator (which maps to the internal @@iterator property).
This iterator function must fulfill the iterator protocol by returning an object with a next function. This next function, when called, must return an object with done and value keys indicating what the current value of the iteration is and whether the iteration is completed:
const validIteratorFunction = () => {
return {
next: () => {
return {
value: null, // Current value of the iteration
done: true // Whether the iteration is completed
};
}
}
};
So, to be utterly clear about this, there are two distinct protocols:
For an object to fulfill the iterable protocol, it must implement [Symbol.iterator], like so:
const zeroToTen = {};
zeroToTen[Symbol.iterator] = function() {
let current = 0;
return {
next: function() {
if (current > 10) return { done: true };
return {
done: false,
value: current++
};
}
}
};
// We can see the effect of the iterable via the spread operator:
[...zeroToTen]; // => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Providing custom methods of iteration via the iterable protocol can be useful when you want to control the order of iteration or if you want to somehow process, filter, or generate values during iteration. Here, for example, we are specifying an iterator function as a generator function, which, as you may recall, returns a generator that fulfills both the iterator and iterable protocols. This generator function will yield two variants for every word stored – one uppercase and one lowercase:
const words = {
values: ['CoFfee', 'ApPLE', 'Tea'],
[Symbol.iterator]: function*() {
for (let word of this.values) {
yield word.toUpperCase();
yield word.toLowerCase();
}
}
};
[...words]
// => ["COFFEE", "coffee", "APPLE", "apple", "TEA", "tea"]
Specifying iterator functions as generator functions like this is far simpler than having to manually implement the iterator protocol. Generators naturally fulfill this contract, so they can be used far more seamlessly. Generators also tend to be more readable and succinct and have the dual benefit of implementing both the iterator and iterable protocols, meaning that they can be used to decorate an object with iteration capabilities:
const someObject = {
[Symbol.iterator]: function*() { yield 123; }
};
[...someObject]; // => [123]
They can, themselves, also provide that iteration capability:
function* someGenerator() {
yield 123;
}
[...someGenerator()]; // => [123]
It's important to keep in mind that any work that's done within a custom iterable should be in line with the expectations of consumers. Iteration is usually considered a read-only operation, so you should steer clear of mutations of the underlying value-set during iteration. Implementing your own iterables can be incredibly powerful, but can also lead to unexpected behavior by the consumers of your code who aren't aware of your custom iteration logic.
It's vital to balance the convenience of custom iteration for those people who are in the know with those people who might only be experiencing your interface or abstraction for the first time.
JavaScript natively supports regular expressions via the object type RegExp, allowing them to be expressed via the literal syntax /foo/ or directly via the constructor (RegExp('foo')). Regular expressions are used to define patterns of characters that can be matched or executed against strings.
Here is an example in which we extract only the long words (>=10 characters) from a corpus of text:
const string = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Etiam sit amet odio ultrices nunc efficitur venenatis laoreet nec leo.';
string.match(/\w{10,}/g); // => ["consectetur", "adipiscing"]
Regular expressions allow us to describe a pattern of characters. They are used for matching and extracting values from strings. For example, if we had a string that contained digits (1,2,3) at various positions, a regular expression would allow us to easily retrieve them:
const string = 'some 1 content 2 with 3 digits';
string.match(/1|2|3/g); // => ["1", "2", "3"]
A regular expression is written as a pattern delimited by forward slashes, with optional flags following the final forward slash:
/[PATTERN]/[FLAGS]
The pattern you write can contain both literal and special characters that together inform the regular expression engine of what to look for. The regular expression we're using in our example contains literal characters (that is, 1, 2, 3) and the pipe (that is, |) special character:
/1|2|3/g
The pipe special character tells the regular expression engine that the characters on the left or the right of the pipe may match. The g, following the final forward slash, is a global flag that directs the engine to search globally within the string and not to give up after the first match is found. For us, this means that our regular expression will match either "1", "2", or "3", wherever they appear within a subject string.
There are specific special characters we can use within regular expressions that act as shortcuts. The notation [0-9] is an example of this. It is a character class that will match all the digits from 0 to 9 so that we don't have to list all of these digits individually. There is also a shorthand character class, \d, that allows us to express this even more succinctly. Thus, we can shorten our regular expression to the following:
/\d/g
For a more realistic application, we may imagine a scenario in which we wish to match sequences of digits, such as phone numbers. Perhaps we wish to match only those phone numbers beginning with 0800 and containing a further 4 to 6 digits. We could do this with the following regular expression:
/0800\d{4,6}/g
Here, we are using the {n, n} syntax, which allows us to express a quantity for the preceding special character, \d. We can confirm that our pattern works by passing it to a test string's match method:
`
This is a test in which exist some phone
numbers like 0800182372 and 08009991.
`.match(
/0800\d{4,6}/g
);
// => ["0800182372", "08009991"]
This brief introduction only touches on the very surface of what regular expressions can do. The syntax of regular expressions allows us to express significant complexity, allowing us to validate that specific text exists within a string or to extract specific text for use within our programs.
The literal syntax of regular expressions allows for specific flags, such as i (ignore-case), to be specified after the final delimiting forward slash. These flags will affect the way the regular expression is executed:
/hello/.test('hELlO'); // => false
/hello/i.test('hELlO'); // => true
When using the RegExp constructor, you can pass your flags as the second argument:
RegExp('hello').test('hELlO'); // => false
RegExp('hello', 'i').test('hELlO'); // => true
There are six available flags in JavaScript's flavor of regular expression:
As we've seen, regular expressions can also be constructed via the RegExp constructor. This can usefully be invoked as both a constructor or a regular function: either way, you'll receive a RegExp object equivalent to what was derived from the literal syntax:
new RegExp('[a-z]', 'i'); // => /[a-z]/i
RegExp('[a-z]', 'i'); // => /[a-z]/i
This is quite a unique behavior. In fact, the RegExp constructor is the only natively provided constructor that can be invoked as both a constructor and a regular function and, in both cases, returns a new instance. You'll recall that the primitive constructors (such as String and Number) can be invoked as regular functions but will behave differently when invoked as constructors.
There are seven methods provided by JavaScript that are capable of utilizing regular expressions:
There are many methods to choose from, but for most situations, you'll likely find that the RegExp method, test(), and the String methods, match() and replace(), are the most useful.
Here's a rundown of some examples of these methods. This should give you an idea of the situations in which each method may be used:
// RegExp.prototype.test
/@/.test('a@b.com'); // => true
/@/.test('aaa.com'); // => false
// RegExp.prototype.exec
const regexp = /\d+/g;
const string = '123 456 789';
regex.exec(string); // => ["123"]
regex.exec(string); // => ["456"]
regex.exec(string); // => ["789"]
regex.exec(string); // => null
// String.prototype.match
'Orders: #92838 #02812 #92833'.match(/\d+/); // => ["92838"]
'Orders: #92838 #02812 #92833'.match(/wo+w/g); // => ["92838", "02812", "92833"]
// String.prototype.matchAll
const string = 'Orders: #92333 <fulfilled> #92835 <pending>';
const matches = [
...string.matchAll(/#(\d+) <(\w+)>/g)
];
matches[0][1]; // => 92333
matches[0][2]; // => fulfilled
// String.prototype.replace
'1 2 3 4'.replace(/\d/, n => `<${n}>`); // => "<1> 2 3 4'
'1 2 3 4'.replace(/\d/g, n => `<${n}>`); // => "<1> <2> <3> <4>'
// String.prototype.search
'abcdefghhijklmnop'.search(/k/); // => 11
// String.prototype.split
'time_in____a__tree'.split(/_+/); // ["time", "in", "a", "tree"]
Most of these methods, as you can see, behave intuitively. However, there is some complexity surrounding stickiness and the lastIndex property, which we will now go over.
By default, if your RegExp is global (that is, uses the g flag), the RegExp methods (that is, test() and exec()) will mutate the lastIndex property of the RegExp object upon each execution. These methods will attempt to match the subject string from the index specified by the current lastIndex property, which is 0 by default, and will then update the lastIndex upon every subsequent call.
This can lead to unexpected behavior if you expect exec() or test() to always return the same result for a given global regular expression and string:
const alphaRegex = /[a-z]+/g;
alphaRegex.exec('aaa bbb ccc'); // => ["aaa"]
alphaRegex.exec('aaa bbb ccc'); // => ["bbb"]
alphaRegex.exec('aaa bbb ccc'); // => ["ccc"]
alphaRegex.exec('aaa bbb ccc'); // => null
It will also lead to confusion if you attempt to execute a global regular expression on more than one string without resetting the lastIndex yourself:
const alphaRegex = /[a-z]+/g;
alphaRegex.exec('monkeys laughing'); // => ["monkeys"]
alphaRegex.lastIndex; // => 7
alphaRegex.exec('birds flying'); // => ["lying"]
As you can see, following the match with the "monkeys" substring, the lastIndex is updated to the next index (7), which means, when executed on a different string, the regular expression will continue where it left off and attempt to match everything beyond that index, which in the case of the second string, "birds flying", is the substring "lying".
As a rule, to avoid these confusions, it's important to always have ownership over your regular expressions. Don't accept regular expressions from elsewhere in a program if you're using RegExp methods. Also, don't attempt to use exec() or test() on different strings without resetting the lastIndex before each execution:
const petRegex = /\b(?:dog|cat|hamster)\b/g;
// Testing multiple strings without resetting lastIndex:
petRegex.exec('lion tiger cat'); // => ["cat"]
petRegex.exec('lion tiger dog'); // => null
// Testing multiple strings with resetting lastIndex:
petRegex.exec('lion tiger cat'); // => ["cat"]
petRegex.lastIndex = 0;
petRegex.exec('lion tiger dog'); // => ["dog"]
Here, you can see that, if we don't reset the lastIndex, our regular expression fails to match on subsequent strings that are passed to the exec() method. If, however, we reset the lastIndex prior to each subsequent exec() call, we'll observe a match.
Stickiness means that a regular expression will try to match at the exact lastIndex and that, if a match is not available at that exact index, it will fail (that is, return null or false, depending on the method used). The sticky flag (y) will force RegExp to read and mutate lastIndex with each match. Traditionally sticky methods such as exec() and test(), as we mentioned previously, will always do this, but the y flag will force stickiness even when using non-sticky methods, such as match():
const regexp = /cat|hat/y; // match 'cat' or 'hat'
const string = 'cat in a hat';
// lastIndex is always zero by default, so will
// match from the start of the string:
regexp.lastIndex; // => 0
regexp.test(string); // => ["cat"]
// lastIndex has been modified following the last
// match but will not match anything as there is
// no cat or hat at index 3:
regexp.lastIndex; // => 3
string.match(regexp); // => null
// Set lastIndex to 9 (index of "hat"):
regexp.lastIndex = 9;
string.match(regexp); // => ["hat"]
Stickiness can be useful if you're looking for a match at a specific index in a string or series of strings. However, its behavior can be unexpected if you're not in full control of lastIndex. As we mentioned previously, a good general rule is to always have ownership over your own RegExp objects so that any mutations to lastIndex are only made by your code.
In this chapter, we have begun to delve into JavaScript by looking at the built-in types that the language provides. The point of our exploration has been to look at these language constructs through the lens of clean code. By doing so, we've highlighted the importance of caution when dealing with some of the more obscure areas of the language. We've discovered many of the nasty edge cases and challenges involved in using JavaScript types, such as the lack of precision in the floating-point Number type and the complexity of Unicode in the String type. Exploring these more difficult parts of the language allows us not only to avoid specific traps but instills a fluency within us that will hugely boost our ability to wield JavaScript in the service of clean code.
In the next chapter, we will continue to enhance this fluency. We will learn more about JavaScript's type system and begin to operate on and manipulate these types to suit our needs.
In the previous chapter, we explored JavaScript's built-in values and types and covered some of the challenges involved when using them. The next natural step is for us to explore how JavaScript's dynamic system plays out in the real world. Since JavaScript is a dynamically typed language, the variables in your code are not constrained in terms of the type of values they refer to. This introduces a huge challenge for the clean coder. Without certainty regarding our types, our code can break in unexpected ways and can become incredibly fragile. This fragility can be explained quite simply by imagining a numeric value embedded within a string:
const possiblyNumeric = '203.45';
Here, we can see that the value is numeric but that it has been wrapped in a string literal and so, as far as JavaScript is concerned, is just a regular string. But because JavaScript is dynamic, we can freely pass this value around to any function—even a function that is expecting a number:
setWidth('203.45');
function setWidth(width) {
width += 20; // Add margins
applyWidth(width); // Apply the width
}
The function adds a margin value to the number via the += operator. This operator, as we will learn later in this chapter, is an alias for the operation a = a + b, and the + operator here will, in the case of either operand being a String type, simply concatenate the two strings together. What's funny is that this simple and innocent-looking implementation detail is the crux of millions of exhausting debugging sessions that have occurred around the world at various times. Thankfully, knowing about this operator and its exact behavior will save you countless hours of pain and exhaustion, and will cement in your mind the importance of writing code that avoids the very trap we've fallen into with our possiblyNumeric value.
In this chapter, we will cover the following topics:
The first crucial step in being able to wrangle our types with more ease is to learn about detection, which is the skill of being able to discern what type or types you're dealing with in the least complex way.
Detection refers to the practice of determining a value's type. Usually, this will be done with the intention of using that determined type to carry out specific behavior such as falling back to a default value or throwing an error in the case of misuse.
Due to JavaScript's dynamic nature, detecting types is an important practice that can often be a great aid to other programmers. If you can usefully throw errors or warnings when someone is using an interface incorrectly, it can mean a much more fluid and speedy flow of development for them. And if you can helpfully populate undefined, null, or empty values with smart defaults, then it'll allow you to provide a more seamless and intuitive interface.
Unfortunately, due to legacies within JavaScript, and some choices made in its design, detecting types can be challenging. A number of different approaches are used, some of which are not considered best practice. We will be going over all of these practices within this section. First, however, it's worth discussing one fundamental question regarding detection: what exactly are you trying to detect?
We often think we require a specific type in order to carry out certain actions, but due to JavaScript's dynamic nature, we may not need to do so. In fact, doing so can lead us to create needlessly restrictive or rigid code.
Consider a function that accepts an array of people objects, like so:
registerPeopleForMarathon([
new Person({ id: 1, name: 'Marcus Wu' }),
new Person({ id: 2, name: 'Susan Smith' }),
new Person({ id: 3, name: 'Sofia Polat' })
]);
In our registerPeopleForMarathon, we may be tempted to implement some kind of check to ensure that the passed argument is of the expected type and structure:
function registerPeopleForMarathon(people) {
if (Array.isArray(people)) {
throw new Error('People is not an array');
}
for (let person in people) {
if (!(person instanceof Person)) {
throw new Error('Each person should be an instance of Person');
}
registerForMarathon(person.id, person.name);
}
}
Are these checks necessary? You may be inclined to say yes as they're ensuring our code is resilient (or defensive) toward potential error cases and is thus more reliable. But if we think about it, none of these checks are necessary to ensure the kind of reliability we're seeking. The intention of our checks, presumably, is to prevent downstream errors in the case that the wrong types or structures are passed to our function, but if we look closely at the preceding code, there are no risks of downstream errors of the types we're worried about.
The first check we conduct is Array.isArray(people) to determine whether the people value is indeed an array. We are doing this, ostensibly, so that we can safely loop through the array. But, as we discovered in the previous chapter, the for...of iteration style is not dependent on its of {...} value being an array. All it cares about is that the value is iterable. An example of this is as follows:
function* marathonPeopleGenerator() {
yield new Person({ id: 1, name: 'Marcus Wu' });
yield new Person({ id: 2, name: 'Susan Smith' });
yield new Person({ id: 3, name: 'Sofia Polat' });
}
for (let person of marathonPeopleGenerator()) {
console.log(person.name);
}
// Logged => "Marcus Wu"
// Logged => "Susan Smith"
// Logged => "Sofia Polat"
Here, we've used a generator as our iterable. This will work just an array would when being iterated over in for...of, so, technically, we could argue that our registerPeopleForMarathon function should accept such values:
// Should we allow this?
registerPeopleForMarathon(
marathonPeopleGenerator()
);
The checks we've made thus far would reject this value as it is not an array. Is there any sense in that? Do you remember the principle of abstraction and how we should be concerned with interface, not implementation? Seen this way, arguably, our registerPeopleForMarathon function does not need to know about the implementation detail of the passed value's type. It only cares that the value performs according to its needs. In this case, it needs to loop through the value via for...of, so any iterable is suitable. To check for an iterable, we might employ a helper such as this:
function isIterable(obj) {
return obj != null &&
typeof obj[Symbol.iterator] === 'function';
}
isIterable([1, 2, 3]); // => true
isIterable(marathonPeopleGenerator()); // => true
Also, consider that we are currently checking that all of our person values are instances of the Person constructor:
// ...
if (!(person instanceof Person)) {
throw new Error('Each person should be an instance of Person');
}
Is it necessary for us to explicitly check the instance in this way? Could we, instead, simply check for the properties that we wish to access? Perhaps all we need to assert is that the properties are non-falsy (empty strings, null, undefined, zero, and so on):
// ...
if (!person || !person.name || !person.id) {
throw new Error('Each person should have a name and id');
}
This check is arguably more specific to our true needs. Checks like these are often called duck-typing, that is, If it walks like a duck and it quacks like a duck, then it must be a duck. We don't always need to check for specific types; we can check for the properties, methods, and characteristics that we're truly dependent on. By doing so, we are creating code that is more flexible.
Our new checks, when integrated into our function, would look something like this:
function registerPeopleForMarathon(people) {
if (isIterable(people)) {
throw new Error('People is not iterable');
}
for (let person in people) {
if (!person || !person.name || !person.id) {
throw new Error('Each person should have a name and id');
}
registerForMarathon(person.id, person.name);
}
}
By using a more flexible isIterable check and employing duck-typing on our person objects, our registerPeopleForMarathon function can now be passed; for example, here, we have a generator that yields plain objects:
function* marathonPeopleGenerator() {
yield { id: 1, name: 'Marcus Wu' };
yield { id: 2, name: 'Susan Smith' };
yield { id: 3, name: 'Sofia Polat' };
}
registerPeopleForMarathon(
marathonPeopleGenerator()
);
This level of flexibility wouldn't have been possible if we had kept our strict type-checking in place. Stricter checks usually create more rigid code and needlessly limit flexibility. There is a balance to strike here, however. We cannot be endlessly flexible. It may even be the case that the rigidity and certainty provided by stricter type-checks enable us to ensure cleaner code in the long run. But the opposite may also be true. The balancing act of flexibility versus rigidity is one you should be constantly considering.
Generally, an interface's expectations should attempt to be as close as possible to the demands of the implementation. That is, we should not be performing detection or other checks unless the checks genuinely prevent errors within our implementation. Over-zealous checking may seem safer but may only mean that future requirements and use cases are more awkward to accommodate.
Now that we've covered the question of why we detect things and exposed some use cases, we can begin to cover the techniques of detection that JavaScript provides us with. We'll begin with the typeof operator.
The first thing you'll often be exposed to when you first try to detect a type in JavaScript is the typeof operator:
typeof 1; // => number
The typeof operator accepts a single operand, to its right-hand-side, and will evaluate to one of eight possible string values, depending on the value that's passed:
typeof 1; // => "number"
typeof ''; // => "string"
typeof {}; // => "object"
typeof function(){}; // => "function"
typeof undefined; // => "undefined"
typeof Symbol(); // => "symbol"
typeof 0n; // => "bigint"
typeof true; // => boolean
If your operand is an identifier without a binding, that is, an undeclared variable, then typeof will usefully return "undefined" instead of throwing a ReferenceError like any other reference to that variable would do:
typeof somethingNotYetDeclared; // => "undefined"
typeof is the only operator in the JavaScript language that does this. Every other operator and every other way of referencing a value will throw an error if that value is not yet declared.
Outside of detecting undeclared variables, typeof is really only useful when determining primitive types—and even that's too generous since not all primitive types are detectable. A null value, for example, when passed to typeof, will evaluate to a rather useless "object":
typeof null; // => "object"
This is an unfortunate and unfixable legacy of the JavaScript language. It will likely never be fixed. To check for null, it is preferred to explicitly check for the value itself:
let someValue = null;
someValue === null; // => true
The typeof operator does not differentiate between different types of objects, except for functions. All non-function objects in JavaScript will return, plainly, "object":
typeof []; // => "object"
typeof RegExp(''); // => "object"
typeof {}; // => "object"
All functions, whether declared via class definitions, method definitions, or plain function expressions, will evaluate to "function":
typeof () => {}; // => "function"
typeof function() {}; // => "function"
typeof class {}; // => "function"
typeof ({ foo(){} }).foo; // => "function"
If typeof class {} evaluating to "function" is confusing, consider that, as we've learned, all classes are just constructor functions with a prepared prototype (which will later determine the [[Prototype]] of any produced instances). There's nothing special about them. Classes are not a unique type or entity within JavaScript.
When it comes to comparing the result of typeof to a given string, we can use either the strict equality (===) or abstract equality (==) operator. Since typeof always returns a string, we don't have to worry about any discrepancies here, so whether you adopt a strict versus abstract equality check is up to you. These would both be fine, technically:
if (typeof 123 == 'number') {...}
if (typeof 123 === 'number') {...}
In conclusion, the typeof operator is only a fair-weather friend. We cannot rely on it in all circumstances. Sometimes, we'll need to use other type detection techniques.
Given the unsuitability of the typeof operator for detecting a number of types, especially objects, we have to rely on a number of different approaches, depending on the exact thing we want to check. Sometimes, we may want to detect a characteristic instead of a type, for example, whether an object is an instance of a constructor or whether it's just a plain object. In this section, we'll be exploring a number of common detection needs and their solutions.
Booleans are thankfully very simple to check. The typeof operator correctly evaluates to "boolean" for values of true and false:
typeof true; // => "boolean"
typeof false; // => "boolean"
It's rare that we'll want to do this, though. Usually, when you are receiving a Boolean value, you are most interested in checking its truthiness rather than its type.
When placing a Boolean value in a Boolean context, such as a conditional statement, we are implicitly relying on its truthiness or falsiness. For example, take the following check:
function process(isEnabled) {
if (isEnabled) {
// ... do things
}
}
This check does not determine whether the isEnabled value is truly Boolean. It just checks that it evaluates to something truthy. What are all the possible values that isEnabled could be? Is there a list of all these truthy values? These values are virtually infinite, so there is no list. All we can say about truthy values is that they are not falsy. And as we know, there are only seven falsy values. If we wish to observe the truthiness or falsiness of specific values, we can always cast to a Boolean via the Boolean constructor invoked as a function:
Boolean(true); // => true
Boolean(1); // => true
Boolean(42); // => true
Boolean([]); // => true
Boolean('False'); // => true
Boolean(0.0001); // => true
In most situations, the implicit coercion to a Boolean is sufficient and won't end up biting us, but if we ever wish to absolutely determine that a value is both Boolean and specifically true or false, we can use the strict equality operator to compare them, like so:
if (isEnabled === true) {...}
if (isEnabled === false) {...}
Due to the dynamic nature of JavaScript, some people prefer this level of certainty but usually, it isn't necessary. If the value we are checking is obviously intended as a Boolean value, then we can use it as so. Checking for its type via typeof or strict equality is usually unnecessary unless there is a possibility that the value may be non-Boolean.
In the case of a Number, we can rely on the typeof operator to correctly evaluate to "number":
typeof 555; // => "number"
However, it will also evaluate to "number" in the case of NaN, Infinity, and -Infinity:
typeof Infinity; // => "number"
typeof -Infinity; // => "number"
typeof NaN; // => "number"
Because of this, we may wish to carry out additional checks to determine whether a number is not any of those values. Thankfully, JavaScript provides native helpers for just this scenario:
Both of the global variants are older parts of the language and, as you can see, are slightly different than their Number.* equivalents. Global isFinite and isNaN cast their values to a number via Number(n), while the equivalent Number.* methods do not do this. The reason for this difference is mostly one of legacy.
The more recently added Number.isNaN and Number.isFinite were introduced to enable more explicit checks without relying on casting:
isNaN(NaN) // => true
isNaN('foo') // => true
Number.isNaN(NaN); // => true
Number.isNaN('foo'); // => false
As you can see, Number.isNaN is more restrictive as it won't cast the value to a Number before checking for NaN. With the 'foo' string, we would need to cast it to Number (and thus evaluate to NaN) before we passed it:
const string = 'foo';
const nan = Number(string);
Number.isNaN(nan); // => true
The global isFinite function works in the same way, that is, it casts its value to a number before checking for finiteness, while the Number.isFinite method will do no casting whatsoever:
isFinite(42) // => true
isFinite('42') // => true
Number.isFinite(42); // => true
Number.isFinite('42'); // => false
If you are confident that your value is already a number, then you may as well use the more succinct isNaN and isFinite as their implicit casting will have no effect on you. And if you'd like for JavaScript to attempt to cast your non-Number value to Number, then you should, once again, use isNaN and isFinite. If, however, you require an explicit check for whatever reason, then you should use Number.isNaN and Number.isFinite.
Combining all of these discussed checks, we are able to confidently detect a number that is neither NaN nor Infinity by using typeof in combination with the global isFinite. As we mentioned previously, isFinite will check for NaN itself, so we needn't bother with an additional isNaN check:
function isNormalNumber(n) {
return typeof n === 'number' && isFinite(n);
}
When it comes to detection, your needs should be driven by the context of your code. For example, it may not be necessary to check for finite numbers if you're embedded in a piece of code where you can safely assume the number is finite. But if you're building a more public API, then you may want to conduct such checks before sending those values down into your internal interfaces, both to reduce the possibilities of bugs and to provide your users with helpful and sensible errors or warnings.
Detecting strings is pleasantly simple. The typeof operator is all we need:
typeof 'hello'; // => "string"
In order to check for the length of a given String, we can simply use the length property:
'hello'.length; // => 5
If we need to check whether a String has a length greater than 0, we can either explicitly do so via length or rely on the falsiness of a 0 length, or even rely on the falsiness of the empty string itself:
const string = '';
Boolean(string); // => false
Boolean(string.length); // => false
Boolean(string.length > 0); // => false
// Since an empty String is falsy we can just check `string` directly:
if (string) { }
// Or we can be more explicit:
if (string.length) { }
// Or we can be maximally explicit:
if (string.length > 0) { }
If we're only checking for the truthiness of a value, then we are also potentially detecting all potential truthy values, including non-zero numbers and objects. To be completely sure that you have a String and that it is not empty, the most succinct technique is as follows:
if (typeof myString === 'string' && myString) {
// ...
}
Emptiness by itself, however, may not be all we're interested in. We may wish to detect whether a string has actual content in it. In most cases, actual content starts at the beginning of the String and ends at the end of the String, but in some cases, it may be embedded within whitespace on either side. To account for this, we can trim the String and then confirm its emptiness:
function isNonEmptyString(string) {
return typeof string === 'string' && string.trim().length > 0;
}
isNonEmptyString('hi'); // => true
isNonEmptyString(''); // => false
isNonEmptyString(' '); // => false
isNonEmptyString(' \n'); // => false
Notice that our function, isNonEmptyString, is using a length > 0 check on the trimmed string instead of just relying on its falsiness as an empty string. This is so that we can safely and confidently know that our isNonEmptyString function will always return a Boolean value. Even though, 99% of the time, it will be used in a Boolean context such as if (isNonEmptyString(...)), we should still ensure that our function has an intuitive and consistent contract.
Strings are simple to detect, but as we mentioned in the previous chapter, working with them can be a challenge due to Unicode. Therefore, it's vital to remember that while the detection of a string may provide us some certainty, it does not tell us what is inside the string and whether it is the value we're expecting. If your detections have the intention of providing a guide or warning to those who are using your interface, you might be better served by explicitly checking the contents of the value.
The undefined type can be checked directly by referring to its globally available value via the strict equality operator:
if (value === undefined) {
// ...
}
Unfortunately, however, since undefined can be overridden within non-global scopes (depending on your precise setup and environment), this approach can be troublesome. Historically, undefined could be overridden globally. This meant that things like this were possible:
let value = void 0; // <- actually undefined
let undefined = 123; // <- cheeky override
if (value === undefined) {
// Does not occur
}
The void operator, as we will explore later, takes one operand to its right-hand side (void foo) and will always evaluate to undefined. As such, void 0 has become a synonym for undefined and is useful as a substitute. So, if you have low confidence in the undefined value, then you can simply check for void 0 like so:
if (value === void 0) {
// value is undefined
}
Various other approaches emerged to ensure a reliable undefined value. One, for example, would simply declare an unassigned variable (which will always default to undefined) and then use that within the scope:
function myModule() {
// My local `undefined`:
const undef;
void 0 === undef; // => true
if (someValue === undef) {
// Instead of `VALUE === undefined` I can
// use `VALUE === undef` within this scope
}
}
Over time, the mutability of the undefined value has been locked down. ECMAScript 2015 forbade global modification, but curiously still allowed local modification.
Thankfully, it has always remained possible to check for undefined via the simple typeof operator:
typeof undefined; // => "undefined"
Using typeof in this way is far less risky than relying on undefined as a literal value, though with the advent of linting tools, it is generally safe to directly check for undefined.
As we've seen, typeof null evaluates to "object". This is an odd legacy of the language. Unfortunately, it means that we cannot rely on typeof for the detection of null. Instead, we must compare to the literal null value directly using a strict quality operator, as shown here:
if (someValue === null) {
// someValue is null...
}
Unlike undefined, null cannot be overwritten in any version of the language, nor in any environment, and so it doesn't come with any headaches around its usage.
So far, we've covered how to independently check for both undefined and null, but we may want to check for both at the same time. It's quite common, for example, to have a function signature that has an optional argument. And if that argument is not passed or is explicitly set to null, it's normal to fall back to some default value. This can be achieved by explicitly checking for both null and undefined, like so:
function printHello(name, message) {
if (message === null || message === undefined) {
// Default to a hello message:
message = 'Hello!';
}
console.log(`${name} says: ${message}`);
}
Often, since both null and undefined are falsy values, it is quite normal to imply their presence by checking the falsiness of a given value:
if (!value) {
// Value is definitely not null and definitely not undefined
}
This, however, will also check whether the value is any of the other falsy values (including, false, NaN, 0, and so on). So, if we want to confirm that a value is specifically null or undefined, and no other falsy value, then we should stick to the explicit variation:
if (value === null || value === undefined) //...
Even more succinctly, however, we can adopt the abstract (non-strict) equality operator to check for either null or undefined since it considers these values to be equal:
if (value == null) {
// value is either null or undefined
}
Although this utilizes the generally frowned-upon abstract equality operator (which we'll explore later in this chapter), it is still a popular way to check for both undefined and null. This is due to its succinct nature. However, adopting this more succinct check makes the code less obvious. It may even leave the impression that the author meant to check solely for null. This ambiguity of intent should leave us doubting its cleanliness. Therefore, in most situations, we should opt for the more explicit and strict check.
Detecting arrays in JavaScript is thankfully very straightforward due to the Array.isArray method:
if (Array.isArray(value)) {
// ...
}
What this method tells us is that the passed value was constructed via the array constructor or an array literal. However, it does not check the [[Prototype]] of the value, so it is entirely possible (although unlikely) that the value, although appearing like an array, may not have the characteristics you desire.
When we believe that we need to check whether a value is an array, it's important to ask ourselves what we're really trying to detect. It may be the case that we can check for the characteristics we desire instead of the type itself. It's crucial to consider what we will be doing with the value. If we are intending to loop over it via for...of, then it may be more suitable for us to check for its iterable-ness instead of its array-ness. As we mentioned earlier, we can employ a helper like this to do so:
function isIterable(obj) {
return obj != null &&
typeof obj[Symbol.iterator] === 'function';
}
const foo = [1,2,3];
if (isIterable(foo)) {
for (let f in foo) {
console.log(f);
}
}
// Logs: 1, 2, 3
If, alternatively, we are looking to use specific array methods such as forEach or map, then it's best to check via isArray as this will give us as a reasonable level of confidence that these methods exist:
if (Array.isArray(someValue)) {
// Using Array methods
someValue.forEach(v => {/*...*/});
someValue.sort((a, b) => {/*...*/});
}
If we were inclined to be really thorough, we could also individually check for specific methods, or we could even force the value into an array of our own so that we could operate on it freely while knowing that the value is truly an array:
const myArrayCopy = [...myArray];
Note that copying an array-like value via the spread syntax ([...value]) will only work if the value is iterable. An example of when using [...value] is appropriate is when operating on NodeLists that have been returned from the DOM API:
const arrayOfParagraphElements = [...document.querySelectorAll('p')];
A NodeList is not a true Array, so it does not give us access to native array methods. Due to this, it is useful to create and use a copy of it that is a true Array.
On the whole, it is safe to adopt and rely on Array.isArray, but it's important to consider whether you even need to check for Array, whether it's more appropriate to check for whether the value is iterable, or even whether it has a specific method or property. As with all other checks, we should seek to make it obvious what our intent is. If we're employing checks that are more obscure than Array.isArray, then it may be prudent to add a comment or abstract the operation away with a descriptively named function.
To detect whether an object is an instance of a constructor, we can simply use the instanceof operator:
const component = new Component();
component instanceof Component;
When we say plain objects, we are typically referring to those that are constructed as either Object literals or via the Object constructor:
const plainObject = {
name: 'Pikachu',
species: 'Pokémon'
};
const anotherPlainObject = new Object();
anotherPlainObject.name = 'Pikachu';
anotherPlainObject.species = 'Pokémon';
This is in contrast to other objects, such as those provided natively by the language (for example, arrays) and those that we construct ourselves via instantiating constructors (for example, new Pokemon()):
function Pokemon() {}
new Pokemon(); // => A non-plain object
The simplest way to detect a plain object is to inquire as to its [[Prototype]]. If it has a [[Prototype]] equal to Object.prototype, then we can say it is plain:
function isPlainObject(object) {
return Object.getPrototypeOf(object) === Object.prototype;
}
isPlainObject([]); // => false
isPlainObject(123); // => false
isPlainObject(new String); // => false
isPlainObject(new Pokemon()); // => false
isPlainObject(new Object()); // => true
isPlainObject({}); // => true
Why would we need to know whether a value is a plain object? It may, for example, be useful to discern plain from non-plain objects when creating an interface or function that accepts configuration objects in addition to more complex object types.
So far, we have learned how to tell the difference between various types and characteristics within JavaScript using detection. As we have seen, detection is useful when needing to provide alternative values or warnings in the case of unexpected or incompatible values. There is an additional mechanism for dealing with such values, however: we can convert them from the values we don't desire into the values we do desire.
In order to convert a value, we use a mechanism known as casting. Casting is the intentional and explicit derivation of one type from another type. In contrast to casting, there is also coercion. Coercion is the implicit and internal process of conversion employed by JavaScript when we use operators or language constructs that require specific types. An example of this would be when passing String values to a multiplication operator. The operator will naturally coerce its String operands to numbers so that it can attempt to multiply them:
'5' * '2'; // => 10 (Number)
The underlying mechanisms in both casting and coercion are identical. They are both mechanisms of conversion. But how we access these low-level behaviors is key. If we do so explicitly, clearly communicating our intent, then the readers of our code will have a far nicer time.
Consider the following code, which contains two different mechanisms for converting a String into a Number:
Number('123'); // => 123
+'123'; // => 123
Here, we are using two different techniques to force the conversion of a value from a String into a Number. The Number() constructor, when called as a function, will internally convert the passed value into a Number primitive. The unary + operator will do the same, although it is arguably less clear. Coercion is even less clear as it often appears to occur as a side effect of some other operation. Here are some examples of this:
1 + '123'; // => "1234"
[2] * [3]; // => 6
'22' / 2; // => 11
The + operator, when either operand is a string, will coerce the opposite operand to a string and then concatenate them both together. The * operator, when given arrays, will call toString() on them and then coerce the resulting String into Number, effectively meaning that [2] * [3] is equal to 2 * 3. Also, the division operator will coerce its operands to numbers before operating on them. All of these coercive behaviors are happening implicitly.
Coercion, since it happens implicitly, can be the cause of many bugs and unexpected behaviors. To avoid this trap, we should always have a strong level of confidence in the types of our operands. Casting, however, is entirely intentional and can help create a more reliable code base. It's common, on the more public or exposed sides of your interfaces, to preemptively cast to the types you desire, just in case the types you've received are not correct.
Observe here how we are explicitly casting both haystack and needle values to the String type:
function countOccurrences(haystack, needle) {
haystack = String(haystack);
needle = String(needle);
let count = 0;
for (let i = 0; i < haystack.length; count++, i += needle.length) {
i = haystack.indexOf(needle, i);
if (i === -1) break;
}
return count;
}
countOccurrences('What apple is the best type of apple?', 'apple'); // => 2
countOccurrences('ABC ABC ABC', 'A'); // => 3
Since we're relying on the indexOf() method on the haystack string, it makes sense, depending on our desired level of defensiveness, to cast the haystack to a string so that we can ensure it has the method available. Casting needle to a string also encodes a higher level of certainty so that we, and fellow programmers, can feel at ease.
Dynamically typed languages such as JavaScript are seen by many as an invitation to chaos. Such people may be used to the comfort and certainty provided by strictly typed languages. In truth, if wielded fully and carefully, a dynamic language can allow our code to be more thoughtfully composed and more resilient to the changing needs of users. In the remainder of this section, we'll be discussing the conversion to individual types, including the explicit casting mechanisms we can utilize and the various coercive behaviors the language adopts internally. We'll begin by looking at Boolean conversion.
All values in JavaScript when converted into a Boolean will return true unless they are one of the seven falsy primitives (false, null, undefined, 0n, 0, "", and NaN), in which case they will return false.
To cast a value to a Boolean, we can simply pass the value to the Boolean constructor, invoking it as a function:
Boolean(0); // => false
Boolean(1); // => true
The language will coerce values to Booleans when the values exist in a Boolean context. Here are some examples of such contexts (each marked with HERE):
This list is not exhaustive. There are quite a few other situations in which our values will be coerced to Booleans. It's usually quite easy to tell. If a language construct or natively-provided function or method allows you to specify one of two possible pathways (that is, if X then do THIS otherwise do THAT), then you can bet that it will be internally coercing whatever value you've expressed to a Boolean.
A common idiom for casting to a Boolean, in addition to the more explicit call to Boolean(), is the double-bang, that is, the unary logical NOT operator (!) repeated twice:
!!1; // => true
!![]; // => true
!!0; // => false
!!""; // => false
Repeating the logical NOT operator twice will invert the Boolean representation of the value twice. It's easier to understand the semantics of the double-bang by seeing it parenthesized:
!( !( value ) )
This is effectively doing four things:
In other words: this does one logical NOT, followed by another, resulting in the Boolean representation of the original value itself.
Explicitly casting values to Booleans is especially useful when you're creating a function or method that must return a Boolean value but deals with values that are not Boolean. For example, I may wish to create an isNamePopulated function that returns false if the name variable is not a populated string or is null or undefined:
function isNamePopulated(name) {
return !!name;
}
This will helpfully return false if name is an empty String, null, or undefined:
isNamePopulated(''); // => false
isNamePopulated(null); // => false
isNamePopulated(undefined); // => false
isNamePopulated('Sandra'); // => true
It will incidentally also return false if name were any other falsy value (such as 0) and it would return true if name were any truthy value:
isNamePopulated(0); // => false
isNamePopulated(1); // => true
This may seem entirely undesirable but, in this context, it may be okay since we're already operating under the assumption that name is a String, null, or undefined, and so we only care about the function's fulfillment of its contract in regards to those values. How comfortable you are with this would depend entirely on your specific implementation and the interface it provides.
Casting a value to a String can be achieved by invoking the String constructor as a regular function (that is, not as a constructor):
String(456); // => "456"
String(true); // => "true"
String(null); // => "null"
String(NaN); // => NaN
String([1, 2, 3]); // => "1,2,3"
String({ foo: 1 }); // => "[object Object]"
String(function(){ return 'wow' }); // => "function(){ return 'wow' }"
Calling String() with your value is the most explicit and clear way of casting to a String, although there are more succinct patterns that are sometimes used:
'' + 1234; // => "1234"
`${1234}`; // => "1234"
These two expressions may appear equivalent, and for many values, they are. But, internally, they work differently. As we'll see later, the + operator will discern whether a given operand is a String by calling its internal ToPrimitive mechanism in such a way that the operand's valueOf (if it has one) will be queried before its toString implementation. However, when using template literals (such as `${value}`), any interpolated values will be converted directly to strings (without going via ToPrimitive). There is always the possibility that a value's valueOf and toString methods will provide different values. Take a look at the following example, which shows how we can manipulate the return values of the two seemingly equivalent expressions by defining our own toString and valueOf implementations:
const myFavoriteNumber = {
name: 'Forty Two',
number: 42,
valueOf() { return number; },
toString() { return name; }
};
`${myFavoriteNumber}`; // => "Forty Two"
'' + myFavoriteNumber; // => 42
This would be a rare situation to encounter but is still worth thinking about. Often, we presume that we can reliably cast any value to a string quite easily, but that may not always be the case.
Traditionally, it's quite common to rely on a value's toString() method and call it directly:
(123).toString(); // => 123
However, if the value is null or undefined, then you'll receive a TypeError:
null.toString(); // ! TypeError: Cannot read property 'toString' of null
undefined.toString(); // ! TypeError: Cannot read property 'toString' of undefined
Additionally, the toString method is not guaranteed to return string. Observe here how we can implement our own toString method that returns Array:
({
toString() { return ['not', 'a', 'string'] }
}).toString(); // => ["not", "a", "string"]
Therefore, it is always advisable to cast to a string via the very explicit and clear String(...). Using indirect forms of coercion, side effects, or blindly relying on toString can create unexpected results. Remember that even if you have a good knowledge of these mechanisms and feel comfortable using them, it doesn't mean other programmers will.
Casting a value to a Number can be achieved by invoking the Number constructor as a regular function:
Number('10e3'); // => 10000
Number(' 4.6'); // => 4.6
Number('Infinity'); // => Infinity
Number('wat'); // => NaN
Number(false); // => 0
Number(''); // => 0
Additionally, there is the unary plus + operator, which does essentially the same thing:
+'Infinity'; // => Infinity
+'55.66'; // => 55.66
+'foo'; // => NaN
These are the only two approaches available for casting a non-Number to a Number type, but JavaScript also provides other techniques for extracting numerical values from strings. One such technique is parseInt, a globally available native function that accepts both a String and an optional radix argument (which defaults to base 10, that is, decimal). It will, naturally, coerce its first argument to a String if it is not already a String and then attempt to extract the first integer of the specified radix from the String. By doing this, you can achieve the following outcomes:
parseInt('1000'); // => 1000
parseInt('100', 8); // => 64 (i.e. octal to decimal)
parseInt('AA', 12); // => 130 (i.e. hexadecimal to decimal)
If the string has a prefix of 0x or 0X, then parseInt will assume the radix to be 16 (hexadecimal):
parseInt('0x10'); // => 16
Some browsers and other environments may also treat a prefix of 0 as an indicator of an octal radix:
// (In *some* environments)
parseInt('023'); // => 19 (assumed octal -> decimal)
parseInt() will also effectively trim the String, ignoring any initial whitespace, and will ignore all the content of the String beyond the first found integer:
parseInt(' 111 222 333'); // => 111
parseInt('\t\n0xFF'); // => 255
In a similar spirit to parseInt there is also a native parseFloat function, which will attempt to extract a float (that is, a floating-point number) from a given String:
parseFloat('42.01'); // => 42.01
parseFloat('\n1e-3'); // => 0.001
parseFloat will trim the string and then look for the longest set of characters from the 0th character that can be naturally parsed by the language in the same way a numeric literal may be parsed. As such, it works fine with Strings that include non-numeric characters beyond a parseable numeric sequence:
parseFloat(' 123 ... rubbish here...'); // => 123
Such a string would cause NaN to be evaluated if we passed it to Number(...). So, in some rare cases, parseFloat may be more useful to you.
Both parseFloat and parseInt will convert their initial argument into a String before attempting extraction. As such, if your first argument is an object, you should be wary of how it may naturally coerce to a string. If your object implements distinct toString and valueOf methods, then you should expect parseInt and parseFloat to only use toString (unless [Symbol.toPrimitive]() is also implemented). This is in contrast to Number(...), which will attempt to convert its argument into a Number directly (without first converting it into a String) and will thus prioritize valueOf over toString:
const rareSituation = {
valueOf() { return "111"; },
toString() { return "999"; }
};
Number(rareSituation); // => 111
parseFloat(rareSituation); // => 999
parseFloat(rareSituation); // => 999
In most situations, converting any value into a Number should be attempted via Number or the unary plus + operator. You should only use parseFloat or parseInt if you have a specific need for their numerical extraction algorithms.
Converting a value into its primitive representation is not something we can do directly, but is done implicitly (that is, coercively) by the language in a number of different situations, such as when you try to use the abstract equality operator, ==, to compare a String, Number, or Symbol to a value that is an Object. The Object, in that scenario, will be converted into its primitive representation via an internal procedure called ToPrimitive, which in summary does the following:
We can see ToPrimitive in action if we attempt a comparison with ==:
function toPrimitive() { return 1; }
function valueOf() { return 2; }
function toString() { return 3; }
const one = { [Symbol.toPrimitive]: toPrimitive, valueOf, toString };
const two = { valueOf, toString };
const three = { toString };
1 == one; // => true
2 == two; // => true
3 == three; // => true
As you can see, if an object has all three methods ([Symbol.toPrimitive], valueOf, and toString), then [Symbol.toPrimitive] will be used. If it has just valueOf and toString, then valueOf will be used. And, of course, if there is only toString, then it will be used.
There is the possibility that 2 and 3 in that procedure will swap if ToPrimitive is called with a hint of String (meaning that it has been instructed to attempt to coerce to a String instead of any primitive). An example of such a case would be when you use a computed member access operator (object[something]), where if something is an object, it would be converted into a String via ToPrimitive with a hint of String, meaning toString() will be attempted before valueOf(). We can see this in action here:
const object = { foo: 123 };
const something = {
valueOf() { return 'baz'; },
toString() { return 'foo'; }
};
object[something]; // => 123
We have both toString and valueOf defined on something, but only toString is used to determine which property to access on object.
If we do not define our own methods, such as valueOf and toString, then the default methods available on the [[Prototype]] of whatever object we're using will be used instead. The primitive representation of an array, for example, is defined by Array.prototype.toString, which will simply join its elements together with a comma as a separator:
[1, 2, 3].toString(); // => "1,2,3"
All types have their own natively provided valueOf and toString methods, so if we wish to force the ToPrimitive internal procedure to use our own methods, then we'll need to override the native ones by supplying our object with its own methods directly or by inheriting from the [[Prototype]]. For example, if you wished to provide a custom array abstraction that had its own primitive conversion behavior, then you could implement it by extending the Array constructor:
class CustomArray extends Array {
toString() {
return this.join('|');
}
}
Then, you'd be able to rely on your CustomArray instances being handled in their own unique way by the ToPrimitive procedure:
String(new CustomArray(1, 2, 3)); // => 1|2|3
new CustomArray(1, 2, 3) == '1|2|3'; // => true
The coercive behaviors of all operators and native language constructs will vary. Any time you pass a value to a language construct or operator that is expecting a primitive (typically either a string or a number), it will likely be passed through ToPrimitive. As such, it's useful to know about this internal procedure. We'll refer to this section as well as we start to explore all of JavaScript's operators in detail.
In this chapter, we have continued to explore the innards of JavaScript, covering the dynamic nature of the language. We've seen how we can go about detecting various types and the nuanced intricacies of coercion and casting. These topics are difficult to pick up, but they will be useful. Many anti-patterns that appear in JavaScript code come down to fundamental misunderstandings of language constructs and mechanisms, so having a deep understanding of these topics will aid our ambition of writing clean code tremendously.
In the next chapter, we will continue our exploration of types by exploring JavaScript's operators. It's likely that you will already have a very good knowledge of many of these, but thanks to JavaScript's dynamic nature, their usage can sometimes yield unexpected results. For this reason, the next chapter dedicates itself fully to the careful exploration of the language's operators.
In the previous chapter on dynamic typing, we explored topics such as type-coercion and detection; we also covered several operators. In this chapter, we'll continue this exploration by delving into every single operator that the JavaScript language makes available. Having a rich understanding of JavaScript's operators will make us feel utterly empowered in a language that can, at times, appear confusing. There is, unfortunately, no shortcut to understanding JavaScript, but as you begin to explore its operators, you will see patterns emerge. For example, many of the multiplicative operators work in a similar manner, as do the logical operators. Once you are comfortable with the main operators, you will begin to see that there is a grace underlying the complexity.
In this chapter, we will cover the following topics:
Now that we're ready to dive in, the very first question we need to ask ourselves is: what even is an operator?
An operator in JavaScript is a standalone piece of syntax that forms an expression and is typically used to derive something or compute a logical or mathematical output from a set of inputs (called operands).
Here, we can see an expression containing an operator (+) with two operands (3 and 5):
3 + 5
Any operator can be said to have four characteristics:
It's important to understand these foundational characteristics as it will vastly aid your usage of operators in JavaScript.
Arity refers to how many operands (or inputs) an operator can receive. An operand is a formal term for the value(s) that you can give or pass to an operator.
If we consider the greater-than operator (>), it receives two operands:
a > b
In this example, a is its first operand (or left-side operand). And b is its second (or right-side operand). Since it receives two operands, the greater-than operator is considered a binary operator. In JavaScript, we have unary, binary, and ternary operators:
// Unary operator examples (one operand)
-a
!a
// Binary operator examples (two operands)
a == b
a >= b
// Ternary operator examples (three operands)
a ? b : c
Knowing about the arity of a given operator is vital—just as it would be vital to know how many arguments to pass a function. It's also important to consider how we are communicating our intent when we compose operations. Since operations can appear in series, it can sometimes be unclear which operator refers to which operand. Consider this confusing expression:
foo + + baz - - bar
To avoid confusion in understanding operations like this, it is conventional to move unary operators closer to their operands and even to employ parentheses to make it absolutely crystal clear what the intent is:
foo + (+baz) - (-bar)
As with all of the parts of code, operators must be wielded with care and concern for the individual or individuals (including your future self) who'll have to encounter, understand, and maintain the code going forward.
An operator's function is simply what it does and what it evaluates to. We'll be going over each operator individually, so there's not a lot to say here beyond a few basic assumptions you can carry with you when working with operators.
In JavaScript, every operator is its own entity and is not tied to the type of operands it is operated on. This is in contrast to some other languages where operators are mapped to overridable functions or are somehow attached to the operands themselves. In JavaScript, operators are their own syntactic entity and have non-overridable functionality. Their functionality is, however, extensible in certain situations.
When using any of the following types of operators, the language will internally attempt coercion:
And to specifically override these mechanisms of coercion, you can supply valueOf(), toString(), or [Symbol.toPrimitive]() methods to any objects you intend to use as operands:
const a = { valueOf() { return 3; } };
const b = { valueOf() { return 5; } };
a + b; // => 8
a * b; // => 15
As we covered in the previous chapter's Conversion to a primitive section, these methods will be called in a specific order depending on the exact operator or language construct used. In the case of all arithmetic operators, for example, valueOf will be attempted before toString.
The order of operation when multiple operators are used in combination is defined by two mechanisms: precedence and associativity. An operator's precedence is a number from 1 to 20, and defines the order in which a series of operators will run. Some operators share the same precedence. Associativity defines the order in which operators of the same precedence will be operated on (either left-to-right or right-to-left).
Consider the following operation:
1 + 2 * 3 / 4 - 5;
In JavaScript, these specific mathematic operators have the following precedences:
And all of them have left-to-right associativity. Since operators of higher precedence occur first and operators of the same precedence will occur according to their associativity, we can say that our example operation occurs in the following order:
If we were to group our operation explicitly using parentheses, it would, therefore, look like this:
(
1 +
(
(2 * 3)
/ 4
)
) - 5;
Every operator, even non-mathematical ones, have specific precedences and associativities. The typeof operator, for example, has a precedence of 16. This can cause a headache if you use it in combination with a lower-precedence operator:
typeof 1 + 2; // => "number2"
Due to the + operator having a lower precedence than typeof, JavaScript would internally run this operation like so:
(typeof 1) + 2;
This, therefore, results in typeof 1 (that is, "number") being concatenated with 2 (producing "number2"). To avoid this, we must force the order using our own parentheses:
typeof (1 + 2); // => "number"
Incidentally, this is why you may often see typeof with parentheses (typeof(...)), which can make it look like a function being invoked. It is, however, an operator, and the parentheses are only there to force a specific order of operation.
Knowing the precedences and associativities of every operator is not something we should expect of our colleagues. It may be reasonable to assume that they know the precedences of some basic mathematical operators, but knowledge beyond that should not be considered guaranteed. Therefore, it is often necessary to provide clarity by employing parentheses even in situations where they may not be strictly necessary. This is especially crucial in complex operations where there are a large number of consecutive operators, as in this example:
function calculateRenderedWidth(width, horizontalPadding, scale) {
return (width + (2 * horizontalPadding)) * scale;
}
Here, the parentheses wrapping (2 * horizontalPadding) is technically unnecessary as the multiplication operator naturally has a higher precedence than the addition operator. However, it is useful to provide extra clarity. Programmers reading this code will be grateful to spend less cognitive energy discerning the exact order of operations. As with many well-intended things, however, this can be taken too far. Parentheses that provide neither clarity nor a different forced order of operation should not be included. An example of such redundancy might be wrapping the entire return expression in additional parentheses:
function calculateRenderedWidth(width, horizontalPadding, scale) {
return ((width + (2 * horizontalPadding)) * scale);
}
This should ideally be avoided, as if it's taken too far, it can introduce additional cognitive load for the reader of the code. A good guide for such situations is if you're inclined to add additional parentheses for clarity, you should probably split the operation into multiple lines:
function calculateRenderedWidth(width, horizontalPadding, scale) {
const leftAndRightPadding = 2 * horizontalPadding;
const widthWithPadding = width + leftAndRightPadding;
const scaledWidth = widthWithPadding * scale;
return scaledWidth;
}
These added lines provide not only clarity around the order of operations but also the purpose of each individual operation by usefully assigning each operation to a descriptive variable.
Knowing every single operator's precedence and associativity is not necessarily vital, but knowing how these mechanisms underly every operation is very useful. Most of the time, as you've seen, it's preferable to split operations into self-contained lines or groups for clarity, even when the internal precedence or associativity of our operators does not demand it. Above all, we must always consider whether we are clearly communicating our intent to the readers of our code.
Knowledge of the mechanisms underlying operators paves the way for us to now explore individual operators within JavaScript. We'll begin by exploring arithmetic and numeric operators.
There are eight arithmetic or numeric operators in JavaScript:
There is one guaranteed outcome of all of these operations that is worth knowing about beforehand. An input of NaN guarantees an output of NaN:
1 + NaN; // => NaN
1 / NaN; // => NaN
1 * NaN; // => NaN
-NaN; // => NaN
+NaN; // => NaN
// etc.
Beyond that basic assumption, each of these operators behaves in a slightly different way, so it's worth going over each of them individually.
The addition operator is a dual-purpose operator:
To accomplish its dual purpose, the + operator first needs to discern whether the operands you've passed can be considered strings. Obviously, a primitive String value is clearly a string, but for non-primitives, the + operator will attempt to convert your operands into their primitive representations by relying on the internal ToPrimitive procedure that we detailed in the last chapter, in the Conversion to a primitive section. If the output of ToPrimitive for either of our + operands is a string, then it will concatenate both operands as strings. Otherwise, it'll add them as numbers.
The fact that the + operator caters to both numeric addition and concatenation can make it quite complicated to understand, so it's helpful for us to walk through a few examples.
Explanation: When both operands are primitive numbers, the + operator very simply adds them together:
1 + 2; // => 3
Explanation: When both operands are primitive strings, the + operator very simply concatenates them together:
'a' + 'b'; // => "ab"
Explanation: When only one operand is a primitive string, the + operator will coerce the other into String and will then concatenate both resulting strings together:
123 + 'abc'; => "123abc"
'abc' + 123; => "abc123"
Explanation: When either operand is a non-primitive, the + operator will convert it into a primitive, and then act as it usually would with that new primitive representation. Here's an example:
[123] + 123; // => "123123"
In this case, JavaScript will convert [123] into its primitive value by using the return value of [123].toString() (that is, "123"). Since the primitive representation of an array is its String representation, the + operator will operate as if we were simply doing "123" + 123, which, as we know, evaluates to "123123".
When using the + operator, it's especially vital to know what operands you're dealing with. If you don't, then the outcome of your operation may be unexpected. The + operator is probably one of the more complex operators since it has a dual purpose. Most operators aren't as complex. The subtraction operator, which we'll explore next, is thankfully far simpler.
The subtraction operator (-) does what it says on the tin. It takes two operands, subtracting the right-side operand from the left-side operand:
555 - 100; // => 455
If either operand is not a number, it will be coerced into one:
'5' - '3'; // => 2
'5' - 3; // => 2
5 - '3'; // => 2
This includes non-primitive types too:
[5] - [3]; // => 2
Here, we're seeing two arrays, each with a single element, being subtracted from each other. This seemingly makes no sense until we recall that the primitive representation of an array is its joined elements as a string, that is, "5" and "3" respectively:
String([5]); // => "5"
String([3]); // => "3"
These will then be converted into their numerical representations, 5 and 3, via an operation that is equivalent to the following:
Number("5"); // => 5
Number("3"); // => 3
Therefore, we are left with the intuitive operation of 5 minus 3, which we know is 2.
The division operator, much like the subtraction operator, accepts two operands that it will coerce to numbers. It will divide its left-side operand by its right-side operand:
10 / 2; // => 5
The two operands are formally called the dividend and the divisor (DIVIDEND / DIVISOR) and will always evaluate according to floating-point math. Integer division does not exist in JavaScript, which means, effectively, that the results of your divisions may always include decimal points, and may hence be liable to the error margin of Number.EPSILON.
Watch out when dividing by zero, as you may end up with either NaN (when dividing zero by zero) or Infinity (when dividing a non-zero number by zero):
10 / 0; // => Infinity
10 / -0; // => -Infinity
0 / 0; // => NaN
If your divisor is Infinity, your division will always evaluate to zero (0 or -0), unless your dividend is also Infinity, in which case, you'll receive NaN:
1000 / Infinity; // => 0
-1000 / Infinity; // => -0
Infinity / Infinity; // => NaN
In circumstances where you expect a divisor or dividend of zero, NaN or Infinity, it is best to be defensive and either check for those values explicitly beforehand or following the operation, like so:
function safeDivision(a, b) {
const result = a / b;
if (!isFinite(result)) {
throw Error(`Division of ${a} by ${b} is unsafe`);
}
return result;
}
safeDivision(1, 0); // ! Throws "Division of 1 by 0 is unsafe"
safeDivision(6, 2); // => 3
The edge cases of division may seem scary but they're not frequently encountered in everyday applications. If we were, however, authoring a medical or financial program, then it'd be absolutely vital to carefully consider our operations' potential error states.
The multiplication operator behaves similarly to the division operator, apart from the obvious fact that it performs multiplication:
5 * 25; // => 125
It is necessary to watch out for effects of coercion and situations where either operand is NaN or Infinity. Rather intuitively, multiplying any non-zero finite value by Infinity will always result in Infinity (with the appropriate sign):
100 * Infinity; // => Infinity
-100 * Infinity; // => -Infinity
However, multiplying zero by Infinity will always result in NaN:
0 * Infinity; // => NaN
-Infinity * -0; // => NaN
Outside of these cases, most usages of the multiplication operator are fairly intuitive.
The remainder operator (%), also known as the modulo operator, is similar to the division operator. It accepts two operands: a dividend, on the left side, and a divisor on the right side. It will return the remainder following an implied division operation:
10 % 5; // => 0
10 % 4; // => 2
10 % 3; // => 1
10 % 2; // => 0
If the divisor is zero, the dividend is Infinity, or either operand is NaN, then the operation will evaluate to NaN:
Infinity % Infinity; // => NaN
Infinity % 2; // => NaN
NaN % 1; // => NaN
1000 % 0; // => NaN
And if the divisor is Infinity, then the result will be equal to the dividend:
1000 % Infinity; // => 1000
0.03 % Infinity; // => 0.03
The modulo operator is useful in situations where you wish to know whether a number goes into another number squarely, such as when wishing to establish the evenness or oddness of an integer:
function isEvenNumber(number) {
return number % 2 === 0;
}
isEvenNumber(0); // => true
isEvenNumber(1); // => false
isEvenNumber(2); // => true
isEvenNumber(3); // => false
As with all other arithmetic operators, it's useful to be aware of how your operands will be coerced. Most usages of the remainder operator are straightforward, so outside of its coercive behaviors and its treatment of NaN and Infinity, you should find its behavior intuitive.
The exponentiation operator (**) takes two operands, a base on the left side and an exponent on the right side. It will evaluate to the base raised to the power of the exponent:
10 ** 2; // => 100
10 ** 3; // => 1,000
10 ** 4; // => 10,000
It is functionally identical to using the Math.pow(a, b) operation, although is more succinct. As with other arithmetic operations, it will internally coerce its operands to the Number type, and passing in any operands of NaN, Infinity, or zero will result in possibly unexpected outcomes, so you should try to avoid such cases.
One curious case worth mentioning is that, if the exponent is zero, then the result will always be 1, regardless of what the base is. So, the base could even be Infinity, NaN, or anything else, and the result would still be 1:
1000 ** 0; // => 1
0 ** 0; // => 1
Infinity ** 0; // => 1
NaN ** 0; // => 1
All other arithmetic operators will evaluate to NaN if one of their operands is NaN, so the behavior of ** here is quite unique. Another unique behavior is that it will throw SyntaxError if your first operand is itself a unary operation:
+2 ** 2;
// SyntaxError: Unary operator used immediately
// before exponentiation expression. Parenthesis
// must be used to disambiguate operator precedence
This is to prevent ambiguity for the programmer. Depending on their previous exposure to other languages (or strict mathematical notation), they may expect cases such as -2 ** 2 to be either 4 or -4. As such, JavaScript will throw in such cases, hence forcing you to be more explicit with either (-2) ** 2 or -(2 ** 2).
Apart from these unique characteristics, the exponentiation operator can be considered similar to other binary (two-operand) arithmetic operators. As always: be aware of your operands' types and how they may be coerced!
The unary plus operator (+...) will convert its operand into Number as if it were passed to Number(...):
+'42'; // => 42
+({ valueOf() { return 42; } });
To do this, our cherished internal ToPrimitive procedure will be utilized, as discussed in the last chapter, in the Conversion to a primitive section. Its result will then be re-coerced into Number if it is not already Number. So, if ToPrimitive were to return String, that String would be converted into Number, meaning that non-numeric strings will result in NaN:
+({ toString() { return 'not a number'; } }); // => NaN
And naturally, if String from ToPrimitive can be converted into Number, then that is what the unary + operator will evaluate to:
+({ toString() { return '12345'; } }); // => 12345
This is more realistically observed when coercing an array via +:
+['5e3']; // => 5000
// Equivalent to:
Number(String(['5e3'])); // => 5000
The unary + operator is usually used in places where a programmer wishes to cast a number-like object to Number so that they can then use it with other numeric operations. Usually, however, it is preferable to explicitly use Number(...) as it is much clearer what the intention is.
The unary + operator can sometimes be confused with other operations. Consider this scenario:
number + +someObject
To someone unfamiliar with the unary plus or someone not attuned to seeing it regularly, this code may look like it contains a typo. We could potentially wrap the entire unary operation in its own parentheses to make it clearer:
number + (+someObject)
Or we could instead use the much clearer Number(...) function:
number + Number(someObject)
In summary, the unary + operator is a convenient shortcut to Number(...). It's useful and quick, though in most cases, we should prefer to communicate our intent more clearly.
The unary minus operator (-...) will first convert its operand into Number in the same way as the unary + operator, detailed in the last section, and will then negate it:
-55; // => -55
-(-55); // => 55
-'55'; // => -55
Its usage is fairly straightforward and intuitive, although, as with unary +, it's useful to disambiguate cases where you have a unary operator next to its binary operator counterpart. Cases like these can be confusing:
number - -otherNumber
It is best, in these situations, to lend clarity with parentheses:
number - (-otherNumber)
The unary minus operator is usually only used directly with a literal number operand to specify a negative value. As with all other arithmetic operators, we should ensure that our intent is clear and that we are not confusing people with long or confusing expressions.
Now that we've explored arithmetic operators, we can begin to look into logical operators.
Logical operators are typically used to build logical expressions where the result of the expression informs some action or inaction. There are three logical operators in JavaScript:
As with most other operators, they can accept a variety of types and will coerce as necessary. The AND and OR operators, unusually, do not always evaluate to a Boolean value, and both utilize a mechanism called short-circuit evaluation to only execute both operands if some condition is met. We'll learn more about this as we explore each individual logical operator.
The NOT operator is a unary operator. It accepts only a single operand and converts that operand into its Boolean representation, then inverts it, so that truthy items become false and falsy items become true:
!1; // => false
!true; // => false
!'hi; // => false
!0; // => true
!''; // => true
!true; // => false
Internally, the NOT operator will perform the following:
As discussed in the Conversion to a Boolean section in the last chapter, a typical idiom for converting a value to its Boolean representation is the double NOT (that is, !!value) as this effectively reverses the truthiness or falsiness of the value twice and evaluates to a Boolean. The more explicit and slightly more preferred idiom is to use Boolean(value), as the intention is far clearer than with !!.
As a result of there being only seven falsy values in JavaScript, the NOT operator can only evaluate to true in these seven scenarios:
!false; // => true
!''; // => true
!null; // => true
!undefined; // => true
!NaN; // => true
!0n; // => true
!0; // => true
JavaScript's rigid definition of falsiness and truthiness is reassuring. It means that even if someone constructs an object with all manner of primitive representations (imagine an object with valueOf() that returns a falsy value), all internal Boolean coercions will still only ever return false for the seven falsy values and nothing else. That means we only need to worry about those seven (it could be much worse...).
On the whole, usage of the logical NOT operator is very straightforward. It's a well-understood syntax across programming languages with clear semantics. As such, there is not a lot in the way of best practices concerning it. At the very least, it's best to avoid too many double negatives in your code. A double negative is when an already negatively-named variable is applied to the NOT operator, like so:
if (!isNotEnabled) {
// ...
}
This is cognitively expensive for those who read your code and is therefore liable to misunderstanding. It's best to use positively-named Boolean variable names so that any logical operations using them are straightforward to comprehend. In this situation, we would simply rename our variable and reverse the operation, like so:
if (isEnabled) {
// ...
}
The logical NOT operator, in summary, is most useful in Boolean contexts such as if() and while(), though is also idiomatically found in the double-NOT !! operation. And it is technically the only operator in JavaScript that is guaranteed to return a Boolean value regardless of its operand's type. Next, we'll explore the AND operator.
The logical AND operator (&&) in JavaScript accepts two operands. If its left-side operand is truthy, then it will evaluate and return the right-side operand; otherwise, it will return the left-side operand:
0 && 1; // => 0
1 && 2; // => 2
It can be a confusing operator for many people because they wrongfully assume that it is equivalent to the question Are both A and B true? when, in fact, it is more akin to If A is truthy then give me B; otherwise, I'll settle for A. People may have an assumption that JavaScript will evaluate both operands, but it in fact will only evaluate the right-side operand if the left-side operand is truthy. This is known as short-circuit evaluation. And JavaScript will not cast the resulting value of the operation to Boolean: instead, it'll just give us that value back, unchanged. If we were to implement the operation ourselves, it might look something like this:
function and(a, b) {
if (a) return b;
return a;
}
Given a simple operation, such as making an if(...) statement conditional upon two values being truthy, the && operator will behave in an entirely unsurprising and expected way:
if (true && 1) {
// Both `true` and `1` are truthy!
}
However, the && operator can be used in more interesting ways too, such as when needing to return a value but only if some prior condition is met:
function getFavoriteDrink(user) {
return user && user.favoriteDrink;
}
Here, the && operator is being used in a non-Boolean context, where there is no coercion of its result occurring. In this case, if its left-side operand is falsy (that is, if user is falsy), then it will return that; otherwise, it will return the right-side operand (that is, user.favoriteDrink):
getFavoriteDrink({ favoriteDrink: 'Coffee' }); // => 'Coffee'
getFavoriteDrink({ favoriteDrink: null }); // => null
getFavoriteDrink(null); // => null
The getFavoriteDrink function behaves in a way that fulfills a basic contract, returning favoriteDrink if the user object is available and if the favoriteDrink property appears on that object, although its actual functionality is a little more chaotic:
getFavoriteDrink({ favoriteDrink: 0 }); // => 0
getFavoriteDrink(0); // => 0
getFavoriteDrink(NaN); // => NaN
Our getFavoriteDrink function is not making any deliberations about the specific nature of the user or favoriteDrink values; it is just blindly yielding to the && operator, returning either its left-side or its right-side operand. If we are confident in the potential values of our operands, then this approach may be fine.
The && operator, thanks to its short-circuiting nature, can also be used to express control flow. Let's consider a scenario in which we wish to call renderFeature() if the isFeatureEnabled Boolean is truthy. Conventionally, we may employ an if statement to do this:
if (isFeatureEnabled) {
renderFeature();
}
But we could also employ &&:
isFeatureEnabled && renderFeature();
This and other unconventional usages of && are typically frowned upon because they can obscure the intention of the programmer and create confusion for readers of your code who may not have such a thorough understanding of how && operates in JavaScript. Nonetheless, the && operator is truly powerful and should be used when well-suited to the task at hand. You should feel empowered to use it as you wish but always be aware of how the typical reader of your code may see the operation and always consider the prospective values that the operation may produce.
The logical OR operator (||) in JavaScript accepts two operands. If its left-side operand is truthy, then it will return that immediately; otherwise, it will evaluate and return the right-side operand:
0 || 1; // => 1
2 || 0; // => 2
3 || 4; // => 3
Much like the && operator, the || operator is flexible in that it does not cast what it returns to Boolean, and it evaluates in a short-circuited manner, meaning that it only evaluates the right-hand side operand if the left-side operand meets a condition—in this case, if the right-side operand is falsy:
true || thisWillNotExecute();
false || thisWillExecute();
Conventionally, a programmer may assume that the logical OR operator is akin to the question Are either A or B true? but in JavaScript, it is more akin to: If A is falsy, then give me B; otherwise, I'll settle for A. If we were to implement this operation ourselves, it might look something like this:
function or(a, b) {
if (a) return a;
return b;
}
Just as with &&, this means that || can be used flexibly to provide control flow or to evaluate specific expressions conditionally:
const nameOfUser = user.getName() || user.getSurname() || "Unknown";
As such, it should be used cautiously in a way that considers what readers of the code are familiar with, and in a way that considers all prospective operands and the resulting values from the operation.
Comparative operators are a collection of binary operators that always return Boolean derived from a comparison between the two operands:
Each of these operators has slightly different functions and coercive behavior so it's useful to go through each of them individually.
The abstract equality (==) and inequality (!=) operators rely on the same algorithm internally, which is responsible for determining whether two values can be considered equal. In this section, our examples will only explore ==, but rest assured that != will always simply be the opposite of whatever == is.
Where both operands, the left-side and the right-side, are of the same type, then the mechanism is quite simple—the operator will check whether the two operands are identical values:
100 == 100; // => true
null == null; // => true
'abc' == 'abc'; // => true
123n == 123n; // => true
Since all non-primitives in JavaScript are of the same type (Object), abstract equality ( ==) will always return false if you try to compare two non-primitives (two objects) that don't refer to the exact same object:
[123] == [123]; // => false
/123/ == /123/; // => false
({}) == ({}); // => false
However, where both operands are of different types, for example, where you are comparing a Number type to a String type or an Object type to a Boolean type, the exact behavior of abstract equality will depend on the operands themselves.
If either operand is Number, and the other is String, then the a == b operation is equivalent to the following:
Number(a) === Number(b)
Here are some examples of this in action:
123 == '123'; // => true
'123' == 123; // => true
'1e3' == 1000; // => true
Continuing down the rabbit hole—if only one operand to the == operator is Boolean, then the operation is, once again, equivalent to Number(a) === Number(b):
false == ''; // => true
// Explanation: Number(false) is `0` and Number('') is `0`
true == '1'; // => true
// Explanation: Number(true) is `1` and Number('1') is `1`
true == 'hello'; // => false
// Explanation: Number(true) is `1` and Number('hello') is `NaN`
false == 'hello'; // => false
// Explanation: Number(false) is `0` and Number('hello') is `NaN`
Finally, if previous conditions are not met, and if either operand is Object (not a primitive), then it will compare the primitive representation of that object to the other operand. As discussed in the last chapter, in the Conversion to a primitive section, this will attempt to call the [Symbol.toPrimitive](), valueOf(), and then toString() methods to establish the primitive. We can see this in action here:
new Number(1) == 1; // => true
new Number().valueOf(); // => 1
({ valueOf() { return 555; }) == 555; // => true
Due to their complicated coercive behaviors, the abstract equality and inequality operators are best avoided. Anyone reading code littered with these operators won't be able to have a good level of confidence in the conditions and control flow of the program because there are simply too many odd edge cases where abstract equality can bite.
The strict equality (===) and strict inequality (!==) operators in JavaScript are a staple of clean code. Unlike their abstract equality cousins, they provide certainty and simplicity in how they treat their operands.
The === operator will only ever return true if both of its operands are identical:
1 === 1; // => true
null === null; // => true
'hi' === 'hi'; // => true
The only exception to this rule is when either operand is NaN, in which case, it'll return false:
NaN === NaN; // => false
No internal coercion will ever occur with strict equality, so even if you have two primitives that could be coerced to the same number, for example, they will still be considered inequal:
'123' === 123; // => false
In the case of non-primitives, both operands must refer to the exact same object:
const me = { name: 'James' };
me === me; // => true
me !== me; // => false
Even if the object is of the equivalent structure or shares other characteristics, if it is not a reference to the same exact object, it'll return false. We can illustrate this by attempting to compare a wrapped Number instance with a value of 3 to the numeric literal, 3:
new Number(3) === 3; // => false
The abstract equality operator (==) would evaluate to true in such a case. You may consider the coercion of new Number(3) to 3 to be preferable, but it's far better to explicitly set up your operands so that they are of the desired types before comparison. So, in the example of String that contains a numerical value that we wish to compare to Number, it would be best to first explicitly cast it via Number():
Number('123') === 123; // => true
It is always advisable to use strict equality instead of abstract equality. It provides far more certainty and reliability in the outcome of every operation and allows you to free your mind from the myriad coercive behaviors that abstract equality entails.
The greater-than (>), less-than (<), greater-than-or-equal-to (>=), and less-than-or-equal-to (<=) operators all operate in a similar manner. They follow an algorithm similar to abstract equality, although how values are coerced is slightly different.
The first thing to note is that all operands to these operators will first be coerced to their primitive representation. Following that, if their primitive representations are both strings, then they'll be compared lexicographically. If their primitive representations are not both strings, then they'll be coerced from whatever they are to numbers and then compared. This means that even if only one of your operands is a string, they'll both be compared numerically.
Lexicographic comparison occurs when both operands are strings, and involves the character-by-character comparison of each string. Broadly, strings that are greater are those that would appear later in a dictionary. Therefore, banana would be lexicographically greater than apple.
As we discovered in Chapter 6, Primitive and Built-In Types, JavaScript uses UTF-16 to encode strings and therefore each codeunit is a 16-bit integer. The UTF-16 codeunits from 65 (U+0041) to 122 (U+007A) are as follows:
ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz
Those characters appearing later are represented by larger UTF-16 integers. To compare any two given codeunits, JavaScript will simply compare their integer values. For the case of comparing B to A, this might look something like this:
const intA = 'A'.charCodeAt(0); // => 65
const intB = 'B'.charCodeAt(0); // => 66
intB > intA; // => true
Every character in each operand string must be compared. To do this, JavaScript will go codeunit-by-codeunit. At each index of each string, if codeunits differ, the larger codeunit will be considered greater, and that string will, therefore, be considered greater than the other:
"AAA" > "AAB"
"AAB" > "AAC"
And if one operand is equal to the prefix of the other, then it will always be considered less than, as shown here:
'coff' < 'coffee'; // => true
As you may have spotted, the lowercase English letters occupy higher UTF-16 integers than uppercase letters. This has the effect of meaning that uppercase is considered less than lowercase and would, therefore, appear before it in a lexicographic ordering:
'A' < 'a'; // => true
'Z' < 'z'; // => true
'Adam' < 'adam'; // => true
You'll also notice that the codeunits from 91 to 96 include the punctuation characters, [\]^_`. This will also affect our lexicographic comparisons:
'[' < ']'; // => true
'_' < 'a'; // => true
Unicode tends to be arranged in a way that any given language's characters will be naturally sorted lexicographically so that the first symbols in a language's alphabet are expressed by lower 16-bit integers than the later symbols. Here, we see, for example, the word for chicken ("ไก่") in Thai is lexicographically less than the word for egg ("ไข่") since the ก character appears before ข in the Thai alphabet:
'ไก่' < 'ไข่'; // => true ("chicken" comes before "egg")
'ก'.charCodeAt(0); // => 3585
'ข'.charCodeAt(0); // => 3586
The natural order of Unicode may not always yield a sensible lexicographic order. As we learned in the previous chapter, complex symbols can be expressed by combining together multiple codeunits into combining character pairs, surrogate pairs (creating code points), or even grapheme clusters. This can create various difficulties. One example would be the following case where a given symbol, in this case, LATIN CAPITAL LETTER A WITH CIRCUMFLEX, can be expressed either via the lone Unicode code-point U+00C2 or via combining the capital letter "A" (U+0041) with the COMBINING CHARACTER ACCENT (U+0302). Symbolically and semantically, these are identical:
'Â'; // => Â
'A\u0302'; // => Â
However, since U+00C2 (decimal: 194) is technically larger than U+0041 (decimal: 65), it will be considered greater than in a lexicographic comparison, even though they are symbolically and semantically identical:
'Â' > 'A\u0302'; // => true
There are thousands of these potential discrepancies to watch out for, so if you ever find yourself needing to compare lexicographically, be mindful that JavaScript's greater-than and less-than operators will be limited by Unicode's inherent ordering.
Numeric comparison using JavaScript's greater-than and less-than operators is fairly intuitive. As mentioned, your operands will be coerced first to their primitive representations, and then coerced a second time, explicitly, to a number. For cases where both operands are numbers, the result is entirely intuitive:
123 < 456; // => true
And for NaN and Infinity, the following assertions can be made:
Infinity > 123; // => true
Infinity >= Infinity; // => true
Infinity > Infinity; // => false
NaN >= NaN; // => false
NaN > 3; // => false
NaN < 3; // => false
If one operand has a primitive representation that is not Number, then it will be coerced to Number before comparison. If you were to accidentally pass Array as an operand to >, then it would first coerce it to its primitive representation, which for arrays, is String with all individual coerced elements joined with a comma, and then attempt to coerce that to Number:
// Therefore this:
[123] < 456;
// Is equivalent to this:
Number(String([123])) < 456
Due to the potentially complicated coercions that may occur, it is always best to pass operands of the same type to >, <, >=, and <=.
The instanceof operator in JavaScript allows you to detect whether an object is an instance of a constructor:
const component = new Component();
component instanceof Component;
This operation will climb the [[Prototype]] chain of its left-side operand looking for a specific constructor function. It will then check whether this constructor is equal to the right-side operand.
Since it climbs the [[Prototype]] chain, it can work safely with multiple inheritances:
class Super {}
class Child extends Super {}
new Super() instanceof Super; // => true
new Child() instanceof Child; // => true
new Child() instanceof Super; // => true
If the right-side operand is not a function (that is, is not callable as a constructor), then TypeError will be thrown:
1 instanceof {}; // => TypeError: Right-hand side of 'instanceof' is not callable
The instanceof operator is sometimes useful in discerning native types such as whether an object is an array:
[1, 2, 3] instanceof Array; // => true
This usage, however, has been largely replaced by Array.isArray(), which is generally more trustworthy as it will work correctly in rare cases where Array has been passed to you from another native context such as a frame (within the browser).
The in operator will return true if a property can be found in an object:
'foo' in { foo: 123 }; // => true
The left-side operand will be coerced to its primitive representation, which, if not Symbol, will be coerced to String. Here, we can see how a left-side operand that is Array will be coerced into a comma-separated serialization of its contents (the native and default way that arrays are coerced to primitives, thanks to Array.prototype.toString):
const object = {
'Array,coerced,into,String': 123
};
['Array', 'coerced', 'into', 'String'] in object; // => true
All seemingly numeric property names in JavaScript are stored as strings, so accessing someArray[0] is equal to someArray["0"], and therefore enquiring as to whether an object has a numeric property with in will also consider both 0 and "0" equally:
'0' in [1]; // => true
0 in { '0': 'foo' }; // => true
When establishing whether a property is in a given object, the in operator will traverse the entire [[Prototype]] chain, hence returning true for all accessible methods and properties at all levels of the chain:
'map' in []; // => true
'forEach' in []; // => true
'concat' in []; // => true
This means that if you're looking to distinguish between the concepts of having a property and having a property on itself, you should instead use hasOwnProperty, a method inherited from Object.prototype that will only check the object itself:
['wow'].hasOwnProperty('map'); // => false
['wow'].hasOwnProperty(0); // => true
['wow'].hasOwnProperty('0'); // => true
On the whole, it is best to only use in if you're confident that there are no collisions with the property names you expect to use and properties provided by the object's [[Prototype]] chain. Even if you're just using plain objects, you still need to worry about the native prototype. If it's been modified in any way (by a utility library, for example), then you can no longer have a high level of trust in the results of your in operations and should hence use hasOwnProperty.
In older library code, you may even find code that chooses not to rely on hasOwnProperty on the object of inquiry, fearing that it may have been overridden. Instead, it'll opt for using the Object.prototype.hasOwnProperty method directly and calls it with that object as its execution context:
function cautiousHasOwnProperty(object, property) {
return Object.prototype.hasOwnProperty.call(object, property);
}
This is likely overly-cautious though. It's safe enough in most code bases and environments to trust and use the inherited hasOwnProperty. The in operator as well, if you've considered the risks, is usually safe enough to use.
An assignment operator will assign the value of its right-side operand to its left-side operand and will return the newly assigned value. The left-side operand of an assignment operation must always be an assignable and valid identifier or property. Examples of this would include the following:
value = 1;
value.property = 1;
value['property'] = 1;
You can additionally use destructuring assignment, which enables you to declare your left-side operand as either an object-literal-like or array-like structure that designates the identifiers you wish to assign and the values you wish to be assigned:
[name, hobby] = ['Pikachu', 'Eating Ketchup'];
name; // => "Pikachu"
hobby: // => "Eating Ketchup"
We will explore destructuring assignment further in a little bit. For now, it's only important to know that it, along with regular identifiers (foo=...) and property accessors (foo.baz = ..., foo[baz] = ...), can be used as the left-side operand to an assignment operator.
There are technically a large number of assignment operators because JavaScript combines regular operators with the basic assignment operator to create more succinct assignment operations in the common case of needing to mutate the value referred to by an existing variable or property. The assignment operators in JavaScript are as follows:
All assignment operators, apart from the direct assignment = operator, will conduct the operation that is indicated by the operator preceding =. So, in the case of +=, the + operator will be applied to the left-and right-side operands, the result of which will then be assigned to the left-side operand. So, consider the following statement:
value += 5
It would be equivalent to:
value = value + 5
The same follows for all other assignment operators that are of the combined type. The addition operator, as we know, will concatenate its operands if either is a string. And the exponentiation operator (**) will always evaluate to 1 if the exponent operand is zero (2 ** 0 === 1). We can rely on this and other existing knowledge to know how such operators will work when combined with assignment. We, therefore, don't need to individually explore all of these assignment operator variants.
Assignment conventionally occurs in the context of a singular line. It's typical to see an assignment statement on its own terminated by a semicolon:
someValue = someOtherValue;
But there's nothing implicit in the assignment operator that requires this. In fact, you can embed an assignment anywhere where you would be able to embed any expression within the language. The following syntax would be entirely legal, for example:
processStep(nextValue += currentValue);
This is carrying out an addition combined with an assignment, and then passing the resulting value to the processStep function. It is exactly equivalent to the following code:
nextValue += currentValue;
processStep(nextValue);
Note here how it is nextValue that is passed to processStep. The result of an assignment operation expression is always the value being assigned:
let a;
(a = 1); // => 1
(a += 2); // => 3
(a *= 2); // => 6
It is common to see assignment in contexts of for and while loops:
for (let i = 0, l = arr.length; i < l; i += 1) { }
// \___/ \____________/ \____/
// | | |
// Assignment Assignment Additive Assignment
This and other patterns of assignment are totally fine as they are so widely used they have become idiomatic of JavaScript. But in most other situations, it is preferable not to embed assignment within other language constructs. Code such as fn(a += b) is potentially unintuitive to some, as it may not be clear what value is actually passed to fn().
These four operators technically fall under the umbrella of assignment but they are unique enough to warrant their own section:
These will simply increment or decrement the value by 1. They're usually found in iteration contexts such as for or while loops. They are best thought of as succinct alternatives to additive and subtractive assignment (that is, value += 1 or value -= 1). However, they have a couple of unique characteristics that are worth covering.
The prefix increment and decrement operators allow you to increment or decrement any given value and will evaluate to the newly incremented value:
let n = 0;
++n; // => 1 (the newly incremented value)
n; // => 1 (the newly incremented value)
--n; // => 0 (the newly decremented value)
n; // => 0 (the newly decremented value)
++n would technically be equivalent to the following additive assignment:
n += Number(n);
Note how the current value of n is first converted into Number. This is the nature of both the increment and decrement operators: they operate strictly on numbers. So, if n were String, that could not be coerced successfully, then the new incremented or decremented value of n would be NaN:
let n = 'foo';
++n; // => NaN
n; // => NaN
Here, we can observe how, since the coercion of foo to a Number fails, the attempted incrementation of it also fails, returning NaN.
The postfix variants of the increment and decrement operators are identical to the prefix variants, except for one fact: the postfix variants will evaluate to the old value, not the newly incremented/decremented value:
let n = 0;
n++; // => 0 (the old value)
n; // => 1 (the newly incremented value)
n--; // => 1 (the old value)
n; // => 0 (the newly decremented value)
This is crucial and can, if not used intentionally, lead to undesirable bugs. Increment and decrement operators are usually used in contexts where this difference is irrelevant. For example, when used in the last expression of a for (_;_;_) statement, the return value is not used anywhere, so we'd see no difference between the two following approaches:
for (let i = 0; i < array.length; i++) { ...}
for (let i = 0; i < array.length; ++i) { ...}
However, in other circumstances, the evaluated value is absolutely key. In the following while loop, for example, the ++i < array.length expression is evaluated on every iteration, meaning that the newly incremented value is compared to array.length. If we swapped this for i++ < array.length, then you'd be comparing the value before incrementing, meaning that it'd be one less and hence we'd get an additional (unwanted!) iteration. You can observe the difference here:
const array = ['a', 'b', 'c'];
let i = -1;
while (++i < array.length) { console.log(i); } Logs: 0, 1, 2
let x = -1;
while (x++ < array.length) { console.log(x); } // Logs: 0, 1, 2, 3
This is quite a rare occurrence, especially with more modern iteration techniques available in the language. But the increment and decrement operators are still very popular in other contexts, so it's useful to appreciate the difference between their prefix and postfix variants.
As briefly mentioned, the left-side operand of an assignment operator (... =) can be specified as a destructuring object or array pattern, like so:
let position = { x: 123, y: 456 };
let { x, y } = position;
x; // => 123
y; // => 456
These patterns typically look like Object or Array literals as they start and end with {} and [] respectively. They are, however, slightly different.
With the destructuring object pattern, where you want to declare the identifier or property you wish to assign to, you must place it as if it were a value in an object literal. That is, where { foo: bar } usually means assign bar to foo, in a destructuring pattern, it means assign the value of foo to the identifier, bar. It is reversed. Where the name of the property of the value you wish to access matches the name that you wish to be assigned in the local scope, you can use a shorter syntax of simply { foo }, as shown here:
let message = { body: 'Dear Customer...' };
// Accessing `body` and assigning to a different name (`theBody`):
const { body: theBody } = message;
theBody; // => "Dear Customer..."
// Accessing `body` and assigning to the same name (`body`):
const { body } = message;
body; // => "Dear Customer..."
For arrays, the slots of syntax where you would usually designate the values (that is, [here, here, and here]) are used to designate the identifiers to which you wish to assign your values, so each identifier in a sequence relates to the same index elements in the array:
let [a, b, c] = [1, 2, 3];
a; // => 1
b; // => 2
c; // => 3
You can also use the rest operator (...foo) to instruct JavaScript to assign the rest of the properties to a given identifier. Here's an example of using it within the destructuring array pattern:
let [a, b, c, ...others] = [1, 2, 3, 4, 5, 6, 7];
others; // => [4, 5, 6, 7];
And here's an example of using it within the destructuring object pattern:
let { name, ...otherThings } = {
name: 'James', hobby: 'JS', location: 'Europe'
};
name; // => "James"
otherThings; // => { hobby: "JS", location: "Europe" }
Destructuring can also occur for object structures that involve multiple levels of hierarchy:
let city = {
suburb: {
inhabitants: ['alice', 'steve', 'claire']
}
};
If we wish to extract the inhabitants array and assign it to a variable of the same name, then we can do the following:
let { suburb: { inhabitants } } = city;
inhabitants; // => ["alice", ...]
And a destructuring array pattern can be embedded in a destructuring object pattern and vice versa:
let {
suburb: {
inhabitants: [firstInhabitant, ...otherInhabitants]
}
} = city;
firstInhabitant; // => "alice"
otherInhabitants: // => ["steve", "claire"]
Destructuring assignment is very useful in avoiding otherwise length, assignments like this:
let firstInhabitant = city.suburb.inhabitants[0];
However, it should be used with reservation as it can sometimes overcomplicate things for those who have to read your code. While it may appear intuitive when writing it for the first time, destructuring assignments are notoriously difficult to untangle. Consider the following statement:
const [{someProperty:{someOtherProperty:[{foo:baz}]}}] = X;
This is cognitively expensive to untangle. It would, perhaps, be more intuitive to express this logic traditionally:
const baz = X[0].someProperty.someOtherProperty[0].foo;
On the whole, destructuring assignment is an exciting and useful feature of the JavaScript language, but it should be used in a guarded way with consideration of the possibility of the confusion it can cause.
Accessing properties in JavaScript is achieved by using one of two operators:
The syntax for directly accessing a property is a single period character, with a left-side operand that is the object you wish to access, and with a right-side operand that is the property name you wish to access:
const street = {
name: 'Marshal St.'
};
street.name; // => "Marshal St."
The right-side operand must be a valid JavaScript identifier, and as such, cannot start with a number, cannot contain whitespace, and in general, cannot contain any punctuation characters that exist elsewhere within the JavaScript specification. You can, however, have properties that are named with so-termed exotic Unicode characters such as π (PI):
const myMathConstants = { π: Math.PI };
myMathConstants.π; // => 3.14...
This is an unconventional practice and is usually only used in novelty settings. It may, however, be genuinely useful in code that is embedded in problem domains where there are legitimate exotic symbols with existing meanings (mathematics, physics, and so on).
In cases where you cannot directly access a property via direct property access, it is possible to compute the property name you wish to access, delimiting it with square brackets:
someObject["somePropertyName"]
It's a right-side operand of any expression, meaning that you can freely compute some value that'll then be coerced to a string (if it is not already a string) and used as the property name to access the object:
someObject[ computeSomethingHere() ]
Typically this is used to access property names that contain characters that make them invalid identifiers, and hence illegal to use with the direct property access operator. This would include numeric property names (such as those found in an array), names with whitespace, or names with punctuation or keywords that exist elsewhere in the language:
object[1];
object['a property name with whitespace'];
object['{[property.name.with.odd.punctuation]}'];
It is best to only rely on computed property access in scenarios when you have no other choice. If there is the possibility of just accessing the property directly (that is, object.property), then you should prefer that. Likewise, if you're deciding what properties an object might contain, it's best to use names that are valid identifiers within the language so they can be directly accessed with ease.
There are a few remaining operators and pieces of syntax that we have yet to explore and that don't fall into any other operator category:
The delete operator can be used to remove properties from objects, as such its only operand usually takes the form of a property accessor, like so:
delete object.property;
delete object[property];
Only properties that are deemed configurable can be deleted in this manner. All properties added conventionally are, by default, configurable and can, therefore, be deleted:
const foo = { baz: 123; };
foo.baz; // => 123
delete foo.baz; // => true
foo.baz; // => undefined
'baz' in foo; // => undefined
However, if the property has been added via defineProperty with configurable set to false, then it'll not be deletable:
const foo = {};
Object.defineProperty(foo, 'baz', {
value: 123,
configurable: false
});
foo.baz; // => 123
delete foo.baz; // => false
foo.baz; // => 123
'baz' in foo; // => true
As you can see, the delete operator evaluates to true or false depending on whether the property has been successfully deleted. Following successful deletion, the property is not merely set to undefined or null but is entirely removed from the object so that checking its existence via in will return false.
The delete operator can technically be used to delete any variable (or so-termed environment record binding internally), but attempting to do so is considered a deprecated behavior and will produce SyntaxError in strict mode:
'use strict';
let foo = 1;
delete foo; // ! SyntaxError
The delete operator has historically been the subject of many inconsistencies between JavaScript implementations, most especially between different browsers. Because of this, only its conventional usage of deleting properties on objects is advisable.
The void operator will evaluate to undefined regardless of its operand. Its operand can be any valid reference or expression:
void 1; // => undefined
void null; // => undefined
void [1, 2, 3]; // => undefined
It doesn't have many uses nowadays, although void 0 is sometimes used as an idiom for undefined either for succinctness or to avoid issues in legacy environments where undefined was an untrusted mutable value.
The new operator is used to form an instance from a constructor. Its right-side operand must be a valid constructor, either provided by the language (for example, new String()) or by ourselves:
function Thing() {}
new Thing(); // => Instance of Thing
By instance, what we truly mean is an object that has a [[Prototype]] equal to the constructor's prototype property, and that has been passed to the constructor as its this binding so that the constructor can fully prepare it for its purpose. Observe here how, whether we define our constructor via a class definition or conventional syntax, we can make the same assertions about the produced instances:
// Conventional Constructor Definition:
function Example1() {
this.value = 123;
}
Example1.prototype.constructor === Example1; // => true
Object.getPrototypeOf(new Example1()) === Example1.prototype; // => true
new Example1().value === 123; // => true
// Class Definition:
class Example2 {
constructor() { this.value = 123; }
}
Example2.prototype.constructor === Example2; // => true
Object.getPrototypeOf(new Example2()) === Example2.prototype; // => true
new Example2().value === 123; // => true
The new operator only cares that its right-side operand is constructible. This means it cannot be a function formed by an arrow function, as in this example:
const Thing = () => {};
new Thing(); // ! TypeError: Thing is not a constructor
As long as you've defined your constructor using a function expression or declaration, it'll work fine. You can even instantiate an anonymous inline constructor if you want:
const thing = new (function() {
this.name = 'Anonymous';
});
thing.name; // => "Anonymous"
The new operator does not formally require the calling parentheses. They only need to be included if you are passing arguments to the constructor:
// Both equivalent:
new Thing;
new Thing();
When you wish to instantiate something and then immediately access a property or method, however, you'd need to disambiguate by providing the calling parentheses and then accessing the property following that; otherwise, you'd receive TypeError:
function Component() {
this.width = 200;
this.height = 200;
}
new Component().width; // => 200
new Component.width; // => ! TypeError: Component.width is not a constructor
(new Component).width; // => 200
The usage of the new operator is usually very straightforward. Semantically, it is understood to relate to the construction of an instance and should therefore ideally only be used to do that. It's also, therefore, assumed that anything being referenced by the right-side operand of new is identified with a name beginning with a capital letter and is a noun. These naming conventions indicate that it is a constructor, providing a useful hint to any programmers who may wish to use it. Here are some examples of both good and bad constructor names:
// Bad (non-idiomatic) names for Constructors:
new dropdownComponent;
new the_dropdown_component;
new componentDropdown;
new CreateDropdownComponent;
// Good (idiomatic) names for Constructors:
new Dropdown;
new DropdownComponent;
The correct naming of a constructor is crucial. It makes our fellow programmers immediately aware of what contract a specific abstraction fulfills. If we name a constructor so that it appears like a regular function, then our colleagues may try to invoke it incorrectly and suffer possible errors as a result. It, therefore, makes perfect sense to take advantage of a name's ability to communicate contract, as discussed in the earlier chapter on naming (Chapter 5, Naming Things is Hard).
The spread syntax (also known as rest syntax) is composed of three dots followed by an operand expression (...expression). It allows the expression to be expanded in places where either multiple arguments or multiple array elements are expected. It technically exists in five distinct areas of the language:
In the context of a function parameter list, the spread syntax must be the very last parameter and would indicate that you wish for all arguments passed to the function from that point onward to be collected into a singular array by the name indicated:
function addPersonWithHobbies(name, ...hobbies) {
name; // => "Kirk"
hobbies; // => ["Collecting Antiques", "Playing Chess", "Drinking"]
}
addPersonWithHobbies(
'Kirk',
'Collecting Antiques',
'Playing Chess',
'Drinking'
);
If you attempt to use it amid other parameters, then you will receive SyntaxError:
function doThings(a, ...things, c, d, e) {}
// ! SyntaxError: Rest parameter must be last formal parameter
In the context of an array literal or a destructuring array pattern, the spread syntax is similarly used to indicate that the values referred to should be spread out. It's best to see these as two opposites, deconstruction and reconstruction:
// Deconstruction:
let [a, b, c, ...otherLetters] = ['a', 'b', 'c', 'd', 'e', 'f'];
a; // => "a"
b; // => "b"
c; // => "c"
otherLetters; // => ["d", "e", "f"]
// Reconstruction:
let reconstructedArray = [a, b, c, ...otherLetters];
reconstructedArray; // => ["a", "b", "c", "d", "e", "f"]
When used in the context of an array literal or a destructuring array pattern, the spread syntax must refer to an iterable value. This doesn't necessarily have to be an array. Strings, as an example, are iterable, so the following also works:
let [...characters] = 'Hello';
characters; // => ["H", "e", "l", "l", "o"]
In the context of an object literal or a destructuring object pattern, the spread syntax is similarly used to spread out all properties of any given object into the receiving object. Once again, we can see this as processes of deconstruction and reconstruction:
// Deconstruction:
const {name, ...attributes} = {
name: 'Nissan Skyline',
engineSize: '2500cc',
year: 2009
};
name; // => "Nissan Skyline"
attributes; // => { engineSize: "2500cc", year: 2009 }
// Reconstruction:
const skyline = {name, ...attributes};
skyline; // => { name: "Nissan Skyline", engineSize: "2500cc", year: 2009 }
When used in this context, the value of the right side of the spread syntax must be either an object or a primitive that can be wrapped as an object (for example, Number or String). This means that all values in JavaScript are permissible except null and undefined, both of which, as we know, cannot be wrapped as objects:
let {...stuff} = null; // => TypeError
It's, therefore, best to only use the spread syntax in an object context when you're confident that the value is an object.
In conclusion, the spread syntax, as we've seen, is remarkably useful in a variety of different situations. Its main advantage is that it reduces the amount of syntax required to extract and designate values.
The comma operator (a, b) accepts a left-side and right-side operand and will always evaluate to its right-side operand. It is sometimes not considered an operator since it does not technically operate on its operands. It's also quite rare.
The comma operator should not be confused with the comma we use to separate arguments when declaring or invoking a function (for example fn(a,b,c)), the comma used when creating array literals and object literals (for example [a, b, c]), or the comma used when declaring variables (for example let a, b, c;). The comma operator is distinct from all of these.
It's most commonly seen in the iteration statement portion of a for(;;) loop:
for (let i = 0; i < length; i++, x++, y++) {
// ...
}
Note how three increment operations are occurring in the third statement (which occurs at the end of each iteration in a conventional for(;;) statement), and that they are each separated by a comma. In this context, the comma is used merely to ensure that all of these individual operations will occur, regardless of each other, within the context of a singular statement. In regular code outside a for(;;) statement, you would likely just have these each dedicated to their own line and statement, like so:
i++;
x++;
y++;
However, due to the constraints of the for(;;) syntax, they must all exist within a singular statement and so the comma operator becomes necessary.
The fact that the comma operator evaluates to its right-side operand is not important in this context, but in other contexts, it may be important:
const processThings = () => (firstThing(), secondThing());
Here, processThings, when invoked, will first call firstThing and then secondThing and will return whatever secondThing returns. It is therefore equivalent to the following:
const processThings = () => {
firstThing();
return secondThing();
};
It is rare to see the comma operator used, even in scenarios like this, as it tends to unnecessarily obscure code that could be more clearly expressed. It's useful to know that it exists and how it behaves, but we shouldn't expect it to be an everyday operator.
Grouping, or parenthesizing, is achieved by using regular brackets ( (...) ). This should not be mistaken for other pieces of syntax that use parentheses, such as function invocation (fn(...)).
The grouping parentheses can be considered an operator just like all of the others we've learned about. They accept one operand—an expression of any form—and will evaluate to whatever resides within them:
(1); // => 1
([1, 2, 3]); // => [1, 2, 3]
(false && true); // => false
((1 + 2) * 3); // => 9
(()=>{}); // => (A function)
As it simply evaluates its contents, you may wonder what the purpose of a group is. Earlier, we covered the concepts of operator precedence and associativity. Sometimes, if you're using a series of operators and wish to force a specific order of operations, then the only way to do that is by wrapping them in a group, which, when used in combination with other operators, has the highest precedence of all:
// The order of operations is dictated
// by each operator's precedence:
1 + 2 * 3 - 5;
// Here, we are forcing the order:
(1 + 2) * (3 - 5);
It's wise to use a group when the order of operations is either not what you desire or has the potential to be unclear to readers of the code. For example, it is sometimes common to wrap items being returned from a function in a group to provide aesthetic containment and clarity:
function getComponentWidth(component) {
return (
component.getInnerWidth() +
component.getLeftPadding() +
component.getRightPadding()
);
}
Another obvious solution to this may be to merely indent the items you wish to contain, but the issue with this is that the JavaScript return statement will not know to look beyond its own line for the start of the expression or value it must return:
// WARNING: this won't work
return
component.getInnerWidth() +
component.getLeftPadding() +
component.getRightPadding();
The return statement in the preceding code effectively terminates itself with a semicolon when the parser observes that there is no value or expression on the same line. This is known as Automatic Semicolon Insertion (ASI) and its existence means we often have to use groups to make it obvious to the parser what our intentions are:
// Clear to humans; clear to the parser:
return (
component.getInnerWidth() +
component.getLeftPadding() +
component.getRightPadding()
);
In summary, grouping is a useful tool for containment and re-ordering operations, and it is a cheap and easy way to increase the clarity and readability of an expression.
JavaScript has seven bitwise operators. The term bitwise here means to operate on binary numbers. These operators are rarely utilized but are useful to know about nonetheless:
All bitwise operators in JavaScript will first coerce their operands (or a singular operand, in the case of bitwise NOT ~) to a 32-bit integer representation. This means that, internally, a number such as 250 would be manifested as follows:
00000000 00000000 00000000 11111010
The last eight bits, in this case of 250, contain all of the information regarding the number:
1 1 1 1 1 0 1 0
+ + + + + + + +
| | | | | | | +---> 0 * 001 = 000
| | | | | | +-----> 1 * 002 = 002
| | | | | +-------> 0 * 004 = 000
| | | | +---------> 1 * 008 = 008
| | | +-----------> 1 * 016 = 016
| | +-------------> 1 * 032 = 032
| +---------------> 1 * 064 = 064
+-----------------> 1 * 128 = 128
=================================
SUM = 250
Adding together all of the bits will get us a decimal integer value of 250.
Every bitwise operator available will operate on these bits and derive a new value. A bitwise AND operation, for example, will yield a bit value of 1 for every pair of bits that are both on:
const a = 250; // 11111010
const b = 20; // 00010100
a & b; // => 16 // 00010000
We can see that only the fifth bit from the right (that is, 16) is on in both 250 and 20, therefore the AND operation will result in only that bit being left on.
Bitwise operators should only be utilized when you are carrying out binary mathematics. Outside of that, any usage of bitwise operators (for example, for side-effects) should be avoided because it drastically limits the clarity and comprehensibility of our code.
It was not uncommon, for a time, to see bitwise operators such as ~ and | being used in JavaScript because they were popular for succinctly deriving the integer floor of a number (for example, ~34.6789 === 34). It goes without saying that this approach, while clever and ego-boosting, created unreadable and unfamiliar code. It remains preferable to use more explicit techniques. In the case of flooring, using Math.floor() is ideal.
In this chapter, we have exhaustively covered the operators available in JavaScript. Collectively, the last past three chapters have given us an incredibly strong foundational understanding of JavaScript syntax, enabling us to feel utterly comfortable when constructing expressions.
In the next chapter, we'll continue to explore the language by applying our existing knowledge of types and operators to the landscapes of declaration and control flow. We'll be exploring how to use larger language constructs to craft clean code and will be discussing many of the traps and idiosyncrasies present in those constructs.
In this chapter, we will continue to explore JavaScript's syntax and constructs. We'll be delving into the fundamentals of expressions, statements, blocks, scopes, and closures. These are the less visible parts of the language. Most programmers assume that they already have a good grasp of how things such as expressions and scopes work, but, as we've seen, our intuitions of how things should work may not always align with how they truly do work. The constructs we'll be learning about in this chapter are the crucial larger building blocks of our programs, so it is of vital importance to understand them fully before we explore more abstract concepts such as control flow and design patterns.
In this chapter we're going to cover the following topics:
There are broadly three types of syntactic container that exist within JavaScript: expressions, statements, and blocks. They are all containers in that they all hold other pieces of syntax and all have distinct behaviors that are worth distinguishing.
It's best to visualize the individual syntactic parts of a program as a hierarchy:

Here, we can see that individual expressions (with a lower border) are wrapped in statements, either of the regular or block variety. It's useful to always have this hierarchical view of the language in our mind as this is how our code will be parsed and understood by the machine. We don't need to see our code as a parser would, of course, but it's indisputably useful to know how our code will be parsed.
This hierarchical view of the language will also help us write programs that communicate their intent well to our fellow programmers. Hierarchy is not only a syntactic concern but a human one. When we write a program, we will typically model problems at different layers of abstraction: each part of a program goes within another part, and from all these individual parts, we can build a program that contains many different layers of complexity.
As we explore the syntactic parts of JavaScript, it's worth remembering how individual elements of a program's syntax, its expressions and statements, will have a natural symmetry with the individual elements and layers of the problem domain.
An expression is the most granular type of syntactic container. We've already been working a lot with expressions. Even expressing a literal value, like the number 1, will produce an expression:
1 // <= An expression containing the literal value 1
Using an operator also forms an expression:
'hi ' + 'there'
In fact, we can consider an operator as something that is itself applied to expressions. So the addition operator's syntax can be understood like so:
EXPRESSION + EXPRESSION
An expression can be as simple as a literal value or a variable reference, but may also be complex. The following expression encompasses a series of operations and is spread over a few lines:
(
'this is part of' +
' ' +
['a', 'very', 'long', 'expression'].join(' ')
)
Expressions are not limited to primitive types or simple literal values. Class definitions, function expressions, array literals, and object literals are all things that can appear in the context of an expression. The easy way to know whether something is an expression is the question of whether or not it can go within a group operator (that is, parentheses) without causing a SyntaxError:
(class Foo {}); // Legal Expression
(function() {}); // Legal Expression
([1, 2, 3]); // Legal Expression
({ a: 1, b: 2 }); // Legal Expression
(if (a) {}); // ! SyntaxError (Not an Expression!)
(while (x) {}); // ! SyntaxError (Not an Expression!)
The syntactic building blocks of any program involve various different layers of syntactic structures. We have individual values and references: if we zoom out a little bit, we have expressions, and if we zoom out even further we have statements, which we will now explore.
A statement contains an expression, and is, therefore, another type of syntactic container. Knowing how JavaScript sees expressions as distinct from statements is hugely helpful in avoiding the various traps and idiosyncrasies of the language.
A statement is formed in a variety of situations. These include the following:
When we place one expression after another, we tend to terminate each individual one with a semicolon. By doing this, we are forming a statement. Explicitly terminating a statement ensures that the JavaScript parser will not have to do so automatically. If you don't use semicolons, then the parser will guess where to insert them via a process called ASI. This process relies on our placement of new lines (that is, \n).
As ASI is automatic, it won't always provide the outcomes you desire. For example, consider the following case where there is a function expression followed by a syntax that is intended as a group (that is, an expression delimited by parentheses):
(function() {})
(
[1, 2, 3]
).join(' ')
This will cause a mysterious TypeError that says: Cannot read property join of undefined. This is because, from the parser's point of view, the following is what we're doing:
(function() {})([1, 2, 3]).join(' ')
Here, we're creating an inline anonymous function and then immediately calling it, passing the [1, 2, 3] array as our sole argument, and then we're attempting to invoke the join method on whatever's returned. But as our function returns undefined, there is no join method there, and so we receive an error. This is a rare situation, but variations of this issue do crop up from time to time. The best way to avoid them is to consistently use semicolons to terminate lines that are intended as statements, as shown in the following code:
(function() {});
(
[1, 2, 3]
).join(' ');
ASI can bite you in other ways as well. A common example is when you attempt to use a return statement within a function, with its intended return value on the next line. In such cases, you'll get a nasty surprise:
function sum(a, b) {
return
a + b;
}
sum(a, b); // => undefined (odd!)
JavaScript's ASI mechanism will presume that the return statement is terminated if there is nothing else present on the same line, and so the following is closer to what the JavaScript engine will see when running the code:
function sum(a, b) {
return;
a + b;
}
To fix this, we can either place a + b on the same line as our return statement or we can use a group operator to contain our indented expression:
function sum(a, b) {
return (
a + b
);
}
It's not necessary to know every ASI rule, but it is very useful to know that it exists. The best way of working with ASI is to avoid it wherever possible. If you're explicit about the termination of your statements, then you won't need to rely on obscure ASI rules, and you won't be relying on your fellow programmers knowing these rules either.
If we consider statements as containers of expressions, then we can consider blocks as containers of statements. In other languages, they are sometimes called compound statements as they allow several statements to exist together.
Blocks are formed by delimiting zero or more statements with curly braces:
{
// I am inside a block
let foo = 123;
}
Blocks are very rarely used as completely isolated units of code (there's very limited benefit from doing so). You'll usually find them within if, while, for and switch statements, as follows:
while (somethingIsTrue()) {
// This is a block
doSomething();
}
The {...} part of the while loop here is a block. It is not an inherent part of the while syntax. If we wish to, we can entirely exclude the block and in its place just have a regular single-line statement:
while (somethingIsTrue()) doSomething();
That would be identical to the version in which we use a block, but obviously this would be limiting if we intend to add more iteration logic. As a result, it's usually preferable to preemptively use a block in such scenarios. Doing so has the added benefit of legitimizing indentation and the containment of the iteration logic.
Blocks are not only syntactic containers. They affect the runtime of our code as well by providing their own scope, which means that we can declare variables within via const and let statements. Observe here how we declare a variable within an if block and how it is not available outside that block:
if (true) {
let me = 'here';
me; // => "here"
}
me; // ! ReferenceError
Scoping is a topic that we should not take lightly. It can be quite difficult to understand, and so what follows is an entire section in which we explore its nature and nuances.
The scope of a given variable can be thought of as the areas within the program where that variable can be accessed.
When we declare a variable at the beginning of a module (outside all functions), we think that it's only natural that this variable should then be accessible to all functions within the module:
var hello = 'hi';
function a() {
hello; // a() can "see" the hello variable
}
function b() {
hello; // b() can "see" the hello variable
}
And if we define a variable within a function, then we expect all inner functions to have access to it:
var value = 'I exist';
function doSomething() {
value; // => "I exist"
}
The fact that we can access value in the doSomething function here is thanks to its scope. The scope of a given variable will depend on how it is declared. When you declare a variable via a var declaration, it will have a different potential scope to a variable created via a let declaration. We will cover these differences soon, but first, it's useful to have a clearer idea of how scopes operate internally.
Internally, when you declare variables, JavaScript will create and store that variable within a lexical environment, which contains the mappings of identifiers to values. A typical JavaScript program can be thought of as having four types of lexical environments, as shown in the following list:
As you know, both functions and blocks can exist within other functions and blocks. Consider the following piece of code that expresses various environments (scopes):
function setupApp(config) {
return {
setupUserProfileMenu() {
if (config.isUserProfileEnabled) {
const onDoneRendering = () => {
console.log('Done Rendering!');
};
// (Do some rendering here...)
onDoneRendering();
}
}
};
}
setupApp({ isUserProfileEnabled: true }).setupUserProfileMenu();
At the point where Done Rendering! is logged, we may expect the hierarchy of environments to look something like this:
Browser Global Environment
\--> Function Environment (setupApp)
\--> Block Environment (if block)
\--> Function Environment (onDoneRendering)
This hierarchy of environments will change throughout the runtime of a given program. If a function is run to completion and its internal scope is no longer used in any exposed internal functions (known as closures), then the lexical environment will be destroyed. Essentially, when a scope is guaranteed to no longer be needed, then JavaScript is free to get rid of it.
A variable declaration occurs via a var keyword followed by a valid identifier or an assignment of the form a = b:
var foo;
var baz = 123;
Variables declared via var are scoped to the nearest function, module, or global environment—that is, they are not block-scoped. At parse time, variable declarations within a given scope will be collected and then, at the point of execution, those declared variables will be hoisted to the top of their execution context and initialized with the undefined value. This means that, within a given scope, technically you can access a variable prior to its assignment, but it'll be undefined:
foo; // => undefined
var foo = 123;
foo; // => 123
The hoisting behavior of var is in contrast to variables declared via let and const, which will produce an ReferenceError if you attempt to access them prior to their declaration:
thing; // ! ReferenceError: Cannot access 'thing' before initialization
let thing = 123;
If you're not careful, the hoisting behavior of var can lead to some unexpected results. For example, there may be a situation where you're attempting to refer to a variable that exists within the outer scope but you are unable to do so because of a variable declaration in your current scope being hoisted:
var config = {};
function setupUI() {
config; // => undefined
var config;
}
setupUI();
Here, the inner scope's variable declaration of config will be hoisted to the top of its scope, meaning that, from the very first line of setupUI, config is undefined.
Since variable declarations are hoisted to the very top of their execution context, even those within a block will be hoisted as if they were first initialized outside of it:
// This:
// (VariableDeclaration inside a Block)
if (true) {
var value = 123;
}
// ... Is equivalent to:
// (VariableDeclaration preceding a Block)
var value;
if (true) {
value = 123
};
In summary, variable declarations create a variable that is scoped to the nearest function, module, or global environment. In the browser, there are no module environments, so it'll either be scoped to its function or the global scope. A variable declaration will be hoisted, before execution, to the top of its respective execution context. This may be the function, the module (in Node.js), or the <script> (in the browser). Variable declarations have fallen out of favor because of the more recently introduced const and let declarations, which are both block-scoped and do not have any odd hoisting behavior.
Let declarations are thankfully far simpler than var declarations. They will be scoped to their nearest environment (whether it is a block, a function, a module, or the global environment) and have no complicated hoisting behaviors.
Their ability to be scoped to a block means that a let declaration inside a block will not have an effect on the outer function scope. In the following code, we can see three different environments (scopes) with a respective place variable in each:
let place = 'outer';
function foo() {
let place = 'function';
{
let place = 'block';
place; // => "block"
}
place; // => "function"
}
foo();
place; // => "outer"
This demonstrates two things to us:
When you use let in either for(;;), for...in, or for...of constructs, even outside of the following block, then that let declaration will be scoped as if it were inside the block. This makes sense intuitively: when we initialize a for loop with let declarations, we naturally expect those to be scoped to the for loop itself and not outside of it:
for (let i = 0; i < 5; i++) {
console.log(i); // Logs: 0, 1, 2, 3, 4
}
console.log(i); // ! ReferenceError: i is not defined
We should only use let if we expect the variable to be reassigned at some later point. If no new assignment will occur, then we should prefer const, as it gives us a little bit of extra peace of mind.
A const declaration has the same characteristics as let, except for one crucial difference: variables declared via const are immutable, meaning that the variable cannot be reassigned to a different value:
const pluto = 'a planet';
pluto = 'a dwarf planet'; // ! TypeError: Assignment to constant variable.
It's important to note that this does not affect the mutability of the value itself. So if the value is any type of object, then all of its properties will retain their mutability:
const pluto = { designation: 'a planet' };
// Assignment to a property:
pluto.designation = 'a dwarf planet';
// It worked! (I.e. the object is mutable)
pluto.designation; // => "a dwarf planet"
Even though const does not protect values from all mutability, it does protect us from some common mistakes and bad practices, such as reusing a variable to refer to several different concepts, or accidentally reassigning a variable because of a typo. The const code phrase is generally safer to use than let, and is now considered the best practice for the declaration of all variables, unless you have an explicit need to reassign a variable after its declaration.
You should also feel free to use const when declaring variables in for...of and for...in iteration constructs, such as in the following case:
for (const n of [4, 5, 6]) console.log(n);
// Logs 4, 5, 6
People often mistakenly opt for using let here because they believe that the looping construct will effectively reassign the variable, making const unsuitable. But in fact, the declaration within for(...) will be tied to a new block scope on each iteration, and thus the const variable will be newly initialized on each iteration within this fresh scope.
In terms of scoping, function declarations behave similarly to variable declarations (that is, var). They will be scoped to their closest function, module, or global environment, and will be hoisted to the top of their respective execution context.
Unlike variable declarations, however, a function declaration will cause the actual assignment of the Function to its identifier to be hoisted as well, meaning that the Function is effectively available before it is declared:
myFunction(); // => "This works!"
function myFunction() { return 'This works!' }
This behavior is quite obscure and as such is inadvisable unless it is very obvious where the definition for myFunction comes from upon invocation. A programmer will typically expect a definition for a function to exist above the place where it is called (or imported as a dependency at some prior point in time), so it can be confusing.
There is further complexity if we consider the possibility of a function declaration residing within a block that is conditionally activated (warning: don't do this!):
giveMeTheBestNumber; // => (Varies depending on implementation!)
if (something) {
function giveMeTheBestNumber() { return 76; }
} else {
function giveMeTheBestNumber() { return 42; }
}
Unfortunately, previous versions of ECMAScript did not prescribe the behavior of function declarations within blocks. This led to various browser implementations choosing their own unique way of handling such situations. Over time, implementations have begun to align. The ECMAScript 2015 specification sensibly forbids either of the giveMeTheBestNumber functions from having their values hoisted. The declaration itself can, however, still be hoisted, meaning that giveMeTheBestNumber would be undefined on lines prior to its declarations (similar to var), as mentioned. This is, at the time of writing, the prevalent behavior of most (but not all) implementations.
Because of the obscurity and the remaining inconsistencies across implementations, it is strongly suggested that you don't use function declarations within blocks. And ideally, it's best not to rely on their hoisting behavior (by referencing function declarations) unless you're confident that doing so would not be misunderstood by those who must read your code.
Inner scopes, as we've seen, have access to the variables of outer scopes:
function outer() {
let thing = 123;
function inner() {
// I can access `thing` within here!
thing; // => 123
}
inner();
}
outer();
What naturally follows from this is the concept of a closure. A closure is how JavaScript enables you to continue to access the scope of an inner function regardless of where or when it is called.
Consider the following function (fn), which returns another function. It has its own scope, in which we declare the coolNumber variable:
function fn() {
let coolNumber = 1;
return function() {
console.log(`
I have access to ${coolNumber}
wherever and whenever I am called
`);
};
}
The inner function, which we return, has access to the coolNumber variable, as we would expect. When we call fn(), its scope is effectively kept alive so that, when we eventually call the inner function, it is still able to access coolNumber.
The following is another example, where we're making use of the continued access to the retained scope (that is, the closure) by reassigning and returning the local variable whenever our inner function is called:
function valueIncrementer() {
let currentValue = 0;
return function() {
return currentValue++;
};
}
const increment = valueIncrementer();
increment(); // => 0
increment(); // => 1
increment(); // => 2
The concept of closures is often over-complicated, so at the risk of doing that, I'll state things quite simply. A closure is not an odd thing, really: it is a natural extension of how we should expect a scope to work. All functions have access to a given scope, so it shouldn't matter how we then pass around these functions after their initial definition. They will continue to have access to that same scope, and are free to access or mutate variables within that scope as they see fit. A function is always anchored to the place it was originally defined and so whether it is called immediately or in a thousand minutes' time, it will have access to the same scope (that is, the same set of lexical environments).
In this chapter, we continued to explore the JavaScript language, zooming out from previous chapters to consider larger pieces of syntax, such as expressions, statements, and blocks. These are programmatic scaffolding components in which we can place the types and operations we've previously learned about. We also covered the complicated mechanisms of scopes, hoisting, and closures. Understanding how these concepts all work together is vital to understanding other people's JavaScript programs and constructing your own.
In the next chapter, we explore how to control flow within JavaScript. This'll allow us to weave together expressions and statements into larger bodies of logic in a clean way. We will then explore the art of abstraction design by learning about design patterns. Though the process of learning these topics individually may appear arduous, by the end of the book you'll have a thorough and powerful understanding of JavaScript that'll enable you to pay less attention to the oddities of the language and more attention to the cleanliness of your code.
This is the final chapter of our exploration into the syntax of JavaScript. We have so far covered its more atomic components its many types, operators, declarations, and statements. Gaining fluency in these is crucial to working effectively with the language at a foundational level and allows us now to take a step back and consider a larger concern: controlling a program's flow. We'll combine all of the syntax we've picked up into clean and understandable programs.
In this chapter, we will cover the following topics:
Control flow refers to the order in which expressions and statements (and entire blocks of code) will run. Programming is, in part, the art of controlling flow. By writing code, we are specifying where control resides at any single moment.
At a granular level, the order of execution is dictated by the individual operators we use in our expressions. We explored the precedence and associativity of operators in the last chapter, discovering that, even if you have a series of operators, one after another, the exact order of their execution is defined by the individual operators' precedence and associativities so that, in the expression, 1 + 2 * 3, the 2 * 3 operation will occur before the addition.
Outside expressions, on the statement level, we control flow in the following ways:
It's easiest to imagine the control flow globally as a type of cursor or finger that is always pointing to a specific expression or statement of code. When a program is executing, the control flow will go down, line by line, until it encounters a piece of syntax that will redirect control to another piece of code. If it encounters an invocation of a function, then that function will be executed in the same manner; the control will be with each consecutive line within the function until it is returned to the caller of the function via a return statement. As control traverses down through a program, each language construct it encounters will be given control over the execution until they each complete. Consider the following simple piece of code:
let basket = [];
for (let i = 0; i < 3; i++) {
basket.push(
makeEgg()
);
}
The flow of control that is taken in the preceding code is as follows:
Even for quite a simple program such as this, the flow can be quite complicated and lengthy. For the benefit of our fellow programmers, it makes sense to try to reduce this complexity whenever possible. The way to accomplish this is via abstraction. Abstracting something won't eliminate that complexity, but it will hide it so that programmers don't need to concern themselves with it. Therefore, before delving into the specific language constructs of control flow in JavaScript, we'll be exploring how these concepts of control flow and abstraction interrelate via the two opposing approaches of imperative and declarative programming.
Imperative programming concerns itself with how something is accomplished, while declarative programming concerns itself with what we want accomplished. It's difficult to see the difference between these so it's best to illustrate them with a simple program:
function getUnpaidInvoices(invoiceProvider) {
const unpaidInvoices = [];
const invoices = invoiceProvider.getInvoices();
for (var i = 0; i < invoices.length; i++) {
if (!invoices[i].isPaid) {
unpaidInvoices.push(invoices[i]);
}
}
return unpaidInvoices;
}
This function's problem domain would be: getting unpaid invoices. That is the task the function has and it is what we want to achieve within the function. This particular function, however, concerns itself a lot with how to achieve its task:
These and other elements of our function are not at all related to the problem domain of getting unpaid invoices. Instead, they are the rather annoying implementation details that we must go through to get our desired output. Functions like this are called imperative because they are mostly concerned with how.
While the imperative form of programming busies itself with procedural low-level steps involved in a task, the declarative form of programming uses abstractions to avoid the use of direct control flow, preferring to express things only in terms of the problem domain itself. The following is a more declarative version of our getUnpaidInvoices function:
function getUnpaidInvoices(invoiceProvider) {
return invoiceProvider.getInvoices().filter(invoice => {
return !invoice.isPaid;
});
}
Here, we are delegating to Array#filter so it handles the specifics of initializing a new array, iteration, and conditional checking. We have freed ourselves from the complexity of conventional control flow by using an abstraction.
Declarative patterns such as this have become the staple of modern JavaScript. They allow you to express the logic you desire at the level of your problem domain, instead of having to worry about lower layers of abstraction such as how to iterate. It's important to see that both declarative and imperative approaches are not completely distinct. They are at either end of a spectrum. On the declarative side of the spectrum, you are operating at a higher level of abstraction, and are hence not exposed to the implementation details that you would be without such abstraction. On the imperative side of the spectrum, you are operating at a lower level of abstraction, utilizing lower-level imperative constructs to tell the machine what you want to accomplish:

Both of these approaches have implications for our control flow. The more imperative approach directly states that it will iterate once through the array and then conditionally push to the output array. The more declarative approach does not make any demands about how the array is iterated through. Naturally, of course, we know that the native Array#filter and Array#map methods will independently iterate through their input arrays, but that is not something we are specifying. All we are specifying is the condition on which our data should be filtered and mapped. How the data is iterated through is completely the concern of the Array#filter and Array#map abstractions.
Imagine we're given of task of conditionally executing a specific piece of code but only if a certain feature is enabled. In our mind, this is how it should work:
if (feature.isEnabled) {
// Do the task.
}
This is the code we want to write, but we later find out that things are not so simple. For starters, there is no isEnabled property for us to use on the feature object. There is, however, a flags array property, which when fully disabled will include Feature.DISABLED_FLAG:
// A feature that is disabled:
feature.flags; // => [Feature.DISABLED_FLAG]
That seems simple enough. But then we discover that, even if the feature does not have this flag and so seems enabled, we also need to check that the time right now aligns with a set of times stored in feature.enabledTimeSlots. If the current time is not in one of the enabled time slots, then we must conclude that the feature is disabled, regardless of whether it has the flag.
This is starting to become quite complicated. In addition to checking for the disabled flag, we'll need to go through these time slots to discover whether the feature is currently enabled based on the current time. So, our simple if statement has very quickly become an unwieldy mess, with several layers of control flow:
let featureIsEnabled = true;
for (let i = 0; i < feature.flags.length; i++) {
if (feature.flags[i] === Feature.DISABLED_FLAG) {
featureIsEnabled = false;
break;
}
}
if (!featureIsEnabled) {
for (let i = 0; i < feature.enabledTimeSlots.length; i++) {
if (feature.enabledTimeSlots[i].isNow()) {
featureIsEnabled = true;
break;
}
}
}
if (featureIsEnabled) {
// Do the task.
}
This is undesirably complex code. It's very far away from the original declarative code we wanted to write. To understand this code, a fellow programmer will have to maintain the state of featureIsEnabled in their head while scanning through each of the individual constructs. This is a mentally burdensome piece of code to navigate through and is, therefore, more liable to misunderstandings, bugs, and general unreliability.
The key question we must now ask ourselves is the following: what would it take for us to abstract away all of these nested layers of control flow away so that we can have our simple if statement back?
We eventually decide to place all of this logic in a newly created isEnabled method within the Feature class—but not only that! We decide to abstract the logic further, by delegating to two internal methods, _hasDisabledFlag and _isEnabledTimeSlotNow. And these methods themselves delegate their iteration logic to array methods, includes(...) and filter(...):
class Feature {
// (Other methods of the Feature class here,..)
_hasDisabledFlag() {
return this.flags.includes(Feature.DISABLED_FLAG);
}
_isEnabledTimeSlotNow() {
return this.enabledTimeSlots.filter(ts => ts.isNow()).length;
}
isEnabled() {
return !this._isDisabledFlag() && this._isEnabledTimeSlotNow();
}
}
These very small declarative additions to the Feature class enable us to write the declarative code we were originally aiming for:
if (feature.isEnabled()) {
// Do the task.
}
This has not only been an exercise in simple abstraction. This has been an exercise in reducing the layers of control flow. We've avoided the need to use nested layers of for if and for blocks, reducing the cognitive burden faced by ourselves and our fellow programmers, and fulfilling the task we originally set out to accomplish in the cleanest way possible.
By carefully refactoring and abstracting our original mess of control flow we have, quite oddly, ended up with a set of code that includes very few traditional control flow statements (if, for, switch, and so on). This doesn't mean our code is without control flow; rather, it means that the control flow is either minimized or hidden away under layers of abstractions. When using the native control flow constructs of the JavaScript language, it is important to remember that they are not your only tool with which to express the flow of a program; you can redirect and split complicated logic into abstractions that each handle a very specific part of your program's flow.
Now that we've got a solid foundational understanding of what control flow is and how it melds with what we know about abstractions, we can go through each of JavaScript's individual control flow mechanisms, highlighting challenges and potential gotchas.
In JavaScript, there are several ways that control can be moved from one piece of code to another. Generally, code will be evaluated from left-to-right and top-to-bottom until it reaches any of the following situations:
Invocation occurs, in its most simple form, by explicitly calling a function. We do this by attaching calling parentheses ((...)) to a value we know to be a function. This value on the left side of (...) can be a direct reference to a variable or property that holds a function or any expression that evaluates to a function:
someFunction();
(function(){})();
someObject.someMethod();
[function(){}][0]();
To construct instances, as we've explored, you can use the new operator. This is also a type of invocation although, in the case of zero arguments, it doesn't technically require calling parentheses:
function MyConstructor() {}
// Both equivalent:
new MyConstructor();
new MyConstructor;
The exact syntax of evaluation before the calling parentheses (on the left side of (...)) is not important as long as it evaluates to a function. If it does not, then you will receive TypeError:
1(); // ! TypeError: 1 is not a function
[](); // ! TypeError: [] is not a function
'wat'(); // ! TypeError: "wat" is not a function
When a function is called, JavaScript will create a new Lexical Environment (a scope) in which that function will be evaluated, and the function will become the current execution context, shifting control from the current area of code to the function's code. This should not be too unintuitive. It makes sense that, in the code, foo();, baz();, and foo() will be given control and will run to completion before baz() is then given control.
A function will return control to you in the following ways:
Invocation can also occur indirectly, via JavaScript's internal mechanisms. For example, in the case of coercion, as explored in the last chapter, methods such as valueOf, toString, or Symbol.toPrimitive may be called in various scenarios. Additionally, JavaScript enables you to define setters and getters so that your custom functionality is activated whenever a given property is accessed or assigned to:
const person = {
set name(name) {
console.log('You are trying to set the name to', name);
}
};
person.name = 'Leo';
// Logs: "You are trying to set the name to Leo"
By assigning to the name property here, we are effectively invoking a function, which itself may then do all manner of things, potentially invoking other functions itself. You can imagine how the control flow of a given program can become potentially incomprehensible when there are many implicit means of invocation such as this. Such implicit mechanisms do have their advantages, but if too much of our problem domain's logic is embedded within such places, then it's less plainly visible to our fellow programmers and hence more likely to cause confusion.
Returning is a shift of control from a function to its caller. It is achieved either via an explicit return statement within the function itself or implicitly when the function runs to completion:
function sayHiToMe(name) {
if (name) {
return `Hi ${name}`;
}
// In the case of a truthy `name` this code is never arrived at
// because `return` exists on a previous line:
throw 'You do not have a name! :(';
}
sayHiToMe('James'); // => "Hi James"
Here, you'll notice that we don't bother placing the implied else condition of a falsy name in its own else block (else {...}) as this would be unnecessary. Because we return when the name is truthy, any code following that return statement will therefore only run in the implied else condition. It's quite common to see such patterns in functions that carry out preemptive input checks:
function findHighestMountain(mountains) {
if (!mountains || !mountains.length) {
return null;
}
if (mountains.length === 1) {
return mountains[0];
}
// Do the actual work of finding the
// highest mountain here...
}
As we see here, returning is not only used to return control to the caller but also for its side-effect: avoiding work that exists on lines below itself in its function. This is often termed returning early and can significantly help to reduce the overall complexity of a function.
Yielding is a shift of control between a generator and its caller. It is achieved by the yield expression, which can optionally designate a value to its right side (the yielded value). It is only valid to use a yield statement within a generator function:
function* makeSomeNumbers() {
yield 645;
yield 422;
yield 789;
}
const iterable = makeSomeNumbers();
iterable.next(); // => {value: 645, done: false}
iterable.next(); // => {value: 422, done: false}
iterable.next(); // => {value: 789, done: false}
If you yield without a value (yield;) then the result will be the same as yielding undefined.
Yielding will force any subsequent calls to the generator function to continue evaluation from the point of yield (as if the yield hadn't occurred). Yielding can be thought of as pausing a function with the prospect of coming back to it later. We can see this in action if we log which part of our generator runs during consecutive calls:
function* myGenerator() {
console.log('Chunk A');
yield;
console.log('Chunk B');
yield;
}
const iterable = myGenerator();
console.log('Calling first time');
iterable.next();
console.log('Done calling first time');
console.log('Calling second time');
iterable.next();
console.log('Done calling second time');
This will log the following:
It is also possible to return from a generator function with a regular return; statement. This is the same as yielding for the final time. That is, no further code will ever be run within that generator.
Yielding is not necessarily a shift of control in just one direction. You can use a generator as a data consumer or observer. In such scenarios, when a caller requests the next yielded value by calling iterable.next(), it can optionally pass an argument to this next() method. Whatever value is passed will then cause the yield expression within the generator to evaluate to that value.
This is more easily explained with an example. Here, we have created a generator that consumes numbers and yields the sum of all numbers previously consumed:
function* createAdder() {
let n = 0;
while (true) n += yield n;
}
const adder = createAdder();
adder.next(); // Initialize (kick things off!)
adder.next(100).value; // => 100
adder.next(100).value; // => 200
adder.next(150).value; // => 350
Here, we are using the return value of our yield expression (yield n) and then adding it to the existing value of n on each run of the generator. We need to call next() once initially to kick things off as, before this, the n += yield n expression has not been run and is hence is not waiting for a next() call yet.
Using generators as consumers does not have many use cases and can be quite an awkward pattern to employ since we must use the designated next() method to pass in data. It is, however, useful to know about the flexibility of the yield expression since you may encounter it in the wild.
For fellow programmers, comprehending the flow of control within generators can be complicated and counter-intuitive since it involves a lot of back-and-forth between the caller and the generator. Knowing what exact code is running at any specific point may be difficult to determine and so it is advisable to keep your generators short and ensure that they yield consistently—in other words, don't have too many different pathways of yielding within your generators and generally attempt to keep cyclomatic complexity quite low (you can read more about this if you skip ahead to the Handling cyclomatic complexity section).
Breaking is a shift of control from within the current for, while, switch, or labeled statement to the code following the statement. It effectively terminates the statement, preventing any following code from being executed.
In the context of iteration, whether or not to continue or break from iteration is usually determined by ConditionExpression within the construct itself (for example, counter < array.length), or by the length of the data structure in the case of for..in and for..of. However, it may still be necessary, at times, to break out of the iteration early.
For example, if you are looking for a specific item within a data structure (a needle-in-a-haystack situation), then it would make sense to stop looking once the item is found. We achieve that by breaking:
for (let i = 0; i < array.length; i++) {
if (myCriteriaIsMet(array[i]) {
happyPath();
break;
}
}
Breaking from an iteration will immediately halt and exit the iteration, meaning any remaining code within the containing IterationBody will not be run. The code immediately following IterationBody will then run.
The break statement is also used to break out from switch statements, typically when you have executed the relevant case statement. As we will discuss later in this chapter, the switch statement will transfer control to the case statement that is considered strictly equal (===) to the value passed to switch(...), and will then run all code following that case statement until an explicit break; (or return;, yield;, or throw;) occurs:
switch (2) {
case 1: console.log(1);
case 2: console.log(2);
case 3: console.log(3);
case 4: console.log(4); break;
case 5: console.log(5);
}
// Logs: 2, 3, 4
Here, we see that a value of 2 shifts control to the matching case 2, and then all of the following code within the switch's body will run naturally until a break; statement is encountered. Hence, we only see logs for 2, 3, and 4. A log for 1 is avoided as case 1 does not match the value, 2, and a log for 5 is avoided as break; occurs before it.
To the right side of the break keyword there may be a label that refers to the switch, for, or while statement. If you don't supply a label, then JavaScript will assume you are referring to the current containing iteration or switch construct. This is only useful when you have two or more breakable constructs within each other, for example, an iteration within an iteration. Observe here how we've labeled our outer for loop with the outerLoop label, enabling us to break out of it from within the inner for loop:
outerLoop: for (let obj in objects) {
for (let key in obj) {
if (/* some condition */) {
break outerLoop;
}
}
}
You can, in fact, break out of any labeled statement (even if it is outside of an iteration or switch construct) but you must explicitly provide the label:
specificWork: {
doSomeSpecificWork();
if (weAreFinished) {
break specificWork;
// immediately exits the `specificWork: {...}` block
}
doOtherWork();
}
This is very rarely applicable but is nonetheless worth knowing about in case you ever run into such code.
One last thing to note on breaking out of iterations or switch statements is that, although we typically do so by using an explicit break; statement, it is something that can also effectively occur via other mechanisms of moving control such as yielding, returning, or throwing. It's quite common, for example, to see an iteration that uses return; to break out not only of itself but also of the containing function.
Continuing is a shift of control from the current statement to the potential start of the next iteration. It is achieved via a continue statement.
Here is an example of continuing conditionally, so that the body of the iteration does not execute for a specific item but the iteration still continues to progress:
const numbers = [1, 2, 3];
for (const n of numbers) {
if (n === 2) continue;
console.log(n);
}
// Logs: 1, 3
Continuing skips all of the code following continue in the current iteration and then moves onto whatever would naturally occur next.
Similar to the break statement, to the right side of the continue keyword can optionally be a label that indicates which iteration construct should be continued. If you don't supply it, then JavaScript will assume you are referring to the current iteration construct. If you have two or more iteration constructs nested within each other, then it may be necessary to use an explicit label:
objectsIteration: for (let obj in objects) {
for (let key in obj) {
if (/* some condition */) {
continue objectsIteration;
}
}
}
Since continuing is a movement of control, we want to remain cautious about how clearly we are communicating our intent. If we have several layers of loops or several continue or break statements, it can burden the reader with an unnecessary level of complexity.
Throwing is a shift of control from the current statement to the nearest containing try...catch statement on the call stack. If no such try...catch statement exists, then the execution of the program will terminate entirely. Throwing is conventionally used to raise exceptions when specific requirements or expectations are not met:
function nameToUpperCase(name) {
if (typeof name !== 'string') {
throw new TypeError('Name should be a string');
}
return name.toUpperCase();
}
To catch this error, we would need to have a try...catch block somewhere on the call-stack, wrapping the call to the nameToUpperCase function or the call to the function that calls it (and so on):
let theUpperCaseName;
try {
theUpperCaseName = nameToUpperCase(null);
} catch(e) {
e.message; // => "Name should be a string"
}
It is a best practice to throw objects that are instances of the natively provided generic Error constructor. There are several native sub-classed constructors of Error:
JavaScript will naturally raise such exceptions to you if you misuse native APIs or produce invalid syntax, but you can also use these constructors yourself to provide more semantically meaningful errors to your fellow programmers. If none of the preceding are suitable, then you can directly use Error or extend from it to produce your own specialized instance, as follows:
class NetworkError extends Error {}
async function makeDataRequest() {
try {
const response = await fetch('/data');
} catch(e) {
throw NetworkError('Cannot fetch data');
}
// ... (process response) ...
}
All Error instances will contain a name and message property. Depending on the JavaScript implementation, there may also be additional properties related to the stack trace of the error. In both the V8 JavaScript engine (used in Chromium and Node.js) and in SpiderMonkey (Mozilla), there is a stack property that gives us serialized call stack information:
try {
throw new Error;
} catch(e) {
e.stack; // => "Error\n at filename.js:2:9"
}
There may be unique situations where you wish to throw a value that is not an Error instance, and technically, this is perfectly legal, but it is rarely useful to do so. It's best to only throw in the case of an actual error, and in that case, it is best to use an appropriate Error object to represent the error.
Now that we've cemented our understanding of how control is moved at a high level, we can delve further into the specific statements and mechanisms that JavaScript gives us to control flow. We'll combine an exploration of the syntax of each statement with some best practices and pitfalls to avoid.
The if statement is composed of the if keyword followed by a parenthesized expression and then an additional statement:
if (ConditionExpression) Statement
ConditionExpression can be of limitless complexity as long as it is truly an expression:
if (true) {}
if (1 || 2 || 3) {}
if ([1, 2, 3].filter(n => n > 2).length > 0) {}
The statement following the parenthesized expression can be a single-line statement or a block and designates the code that should be run if the ConditionExpression evaluates to a truthy value:
// These are equivalent
if (true) { doBaz(); }
if (true) doBaz();
The value you pass as ConditionExpression is compared to a Boolean to determine its truthiness. We've already been aptly introduced to the concepts of truthiness and falsiness in Chapter 6, Primitive and Built-In Types, but just in case you're rusty: there are only seven falsy values in JavaScript, and as such, only seven possible values that you can pass to an if statement that won't satisfy it:
if (false) {}
if (null) {}
if (undefined) {}
if (0n) {}
if (0) {}
if ('') {}
if (NaN) {}
When an if statement is not satisfied, it will run an optional else statement, which you may specify immediately following your if statement. Just as with if, you may use a block here as well:
if (isLegalDrinkingAge) drink(); else leave();
// Equivalent, with Blocks:
if (isLegalDrinkingAge) {
drink();
} else {
leave();
}
You can effectively chain together if/else statements as follows:
if (number > 5) {
// For numbers larger than five
} else if (number < 3) {
// For numbers less than three
} else {
// For everything else
}
Syntactically, it's important to understand that this isn't a construct of its own (there is no such thing as an if/else/if/else construct); it is merely a regular if statement, followed by an else statement that itself contains its own if/else duo. Therefore, it is more accurate, perhaps, to see it as follows:
if (number > 5) {
// For numbers larger than five
} else {
if (number < 3) {
// For numbers less than three
} else {
// For everything else
}
}
An if statement is best suited for when there are one or two possible outcomes of a condition. If there are more possible outcomes, then you may be better off using a switch statement. Long if/else chains can get unwieldy. See the Handling cyclomatic complexity section later in this chapter to explore other novel ways of handling complex conditional logic.
The for statement is used to iterate through a set, typically, an array or any iterable structure. It comes in four broad varieties:
The type of for construct you'll employ will depend on what exactly you wish to iterate over. For straightforward indexed and array-like structures, for example, the for...of construct will be most useful. We'll go over each of these constructs to explore use cases and potential challenges.
The conventional for statement is used to iterate over all manner of data structures or conceptual looping scenarios. It includes three expressions, parenthesized and separated by semicolons, and a statement at the end, which is considered the body of the iteration:
for (
InitializerExpression;
ConditionExpression;
UpdateExpression
) IterationBody
The purpose of each part is as follows:
Using the conventional for statement to loop over an array would look like this:
for (let i = 0; i < array.length; i++) {
array[i]; // => (Each `array` item)
}
It is preferable to use for...of if you're just iterating over a regular array or iterable structure. However, if you need to iterate over a structure indexed unconventionally, then it may be appropriate to use the conventional for loop.
An example of an unconventionally indexed structure is the pixel data of a <canvas> element, which forms an array containing the RGBA (Red, Green, Blue, and Alpha) values of every pixel consecutively, like so:
[r, g, b, a, r, g, b, a, ...]
Since each individual pixel occupies four elements of the array, we would need to iterate over it four indexes at a time. The conventional for loop is perfectly suited to this:
const pixelData = canvas.getContext('2d').getImageData(0, 0, 100, 100).data;
for (let i = 0; i < pixelData.length; i += 4) {
let red = pixelData[i];
let blue = pixelData[i + 1];
let green = pixelData[i + 2];
let alpha = pixelData[i + 3];
// (do something with RGBA)
}
The conventional for statement is a well understood and idiomatic piece of syntax. It is best to ensure that you use each of its parts for its purpose. It is entirely possible (though unadvisable) to exploit its syntax by including the actual logic of your iteration in the parenthesized portion of the construct, but this and other misuses can be quite hard to parse for humans:
var copy = [];
for (
let i = 0;
i < array.length;
copy[i] = array[i++]
);
UpdateExpression here includes the copy[i] = array[i++] expression, which will copy across the element of the array at the current index and will then increment the index. The postfix ++ operator ensures that the previous value of its operand will be returned, guaranteeing that the index accessed on copy[i] is always equal to array[i++]. This is a clever but rather obscure syntax. It would have been far clearer to use the idiomatic for structure, which places the iteration logic in its own statement after for(...):
for (
let i = 0;
i < array.length;
i++
) {
copy[i] = array[i];
}
This is a more familiar and comprehensible piece of code for most programmers. It is more verbose, and perhaps not as fun to write, but in the end, as explored in the initial chapters of this book, we are most interested in writing code that communicates its intent clearly.
The for...in construct is used to iterate over an object's set of enumerable property names. It has the following syntax:
for (LeftSideAssignment in Object) IterationBody
The various parts have the following constraints:
The for...in construct is usually used to iterate through a plain object's properties:
const city = { name: 'London', population: 8136000 };
for (const key in city) {
console.log(key);
}
// Logs: "name", "population"
You can see that we're using const key here to initialize our key variable on each iteration. This is the preferred declaration to use unless you have a specific need for the mutable behavior of let or the different scoping behavior of var. Naturally, all of these declarations are perfectly valid to use, in addition to using no declaration whatsoever:
for (let key in obj) {}
for (var key in obj) {}
for (const key in obj) {}
for (key in obj) {}
A new block scope is created for each iteration. When you use either a let or const declaration, it will be scoped to that iteration, while a variable declared via var will, as we know, be scoped to the nearest execution context's scope (function scope). Using no declaration whatsoever is fine, but you should ensure that you have already initialized that identifier beforehand:
let key;
for (key in obj) {}
Since anything that would be valid on the left side of an assignment expression is valid on the left side of in, we can also place a property reference here, as in the following example:
let foo = {};
for (foo.key in obj) {}
This would result in foo.key being assigned each key of obj as the iteration progresses. This would be quite an odd thing to do, but will nonetheless work correctly.
Now that we have the syntax out of the way, we can discuss the behavior and use cases of for..in. It is, as mentioned, useful in iterating through the properties of an object. By default, this will include all properties inherited from the object's [[Prototype]] chain as well, but only if they are enumerable:
const objectA = { isFromObjectA: true };
const objectB = { isFromObjectB: true };
Object.setPrototypeOf(objectB, objectA);
for (const prop in objectB) {
console.log(prop);
}
// Logs: "isFromObjectB", "isFromObjectA"
As you can see, properties on the object itself are iterated over before those from inherited objects. The order of iteration, however, should not be depended upon as this may differ between implementations. If you're looking to iterate through a set of keys in a specific order, it may be better instead to gather the keys via Object.keys(obj) and then iterate over that as an array.
Since for...in will naturally iterate over inherited properties, it's conventional to place an additional check within the iteration body to avoid these properties:
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
// `key` is a non-inherited (direct) property of `obj`
}
}
Where you have an iterable object (such as an array), it is advisable to use for...of instead, which is more performant and idiomatic for such situations.
The for...of construct is used to iterate over an iterable object. Natively provided iterable objects include String, Array, TypedArray, Map, and Set. Syntactically, for...of shares the characteristics of for...in:
for (LeftSideAssignment in IterableObject) IterationBody
The purpose of each part is as follows:
An idiomatic for...of usage may look like this:
const array = [1, 2, 3];
for (const i of array) {
console.log(i);
}
// Logs: 1, 2, 3
The scoping behavior of let, var, and const is identical to that described in the last section on for...in. It is advisable to use const as it will initialize a fresh and immutable variable for each iteration. Using let is not awful but, unless you have a specific reason to need to mutate the variable yourself within IterationBody, you'll be better off using const.
The while statement is used to run a piece of code until some condition stops being met. It has the following syntax:
while (ConditionExpression) IterationBody
The purpose of each part is as follows:
It is rare to use while for straightforward iteration because there are more suitable constructs for this (for example, for...of), but if we wanted to, it might look something like the following:
const array = ['a', 'b', 'c'];
let i = -1;
while (++i < array.length) {
console.log(array[i]);
}
// Logs: 'a', 'b', 'c'
Since we are initializing i to -1 and are using the prefix increment operator (++i), ConditionExpression will evaluate to 0 < array.length, 1 < array.length, 2 < array.length, and 3 < array.length. Naturally, the last check will fail as 3 is not less than array.length, meaning that the while statement will stop running its IterationBody. This means Body will only 3 times in total.
It's common to use while when the limit of iteration is, as yet, unknown or computed in a complex fashion. In such instances, it is common to see true directly passed as ConditionExpression to while(...) and then a manual break; statement within the iteration to force it to end:
while (true) {
if (/* some custom condition */) {
break;
}
}
The while statement is also used in the context of generator functions if those generators are intended to produce infinite outputs. For example, you may wish to create a generator that always produces the next letter in an alphabet, and then loops round to the start of the alphabet when it gets to z:
function *loopingAlphabet() {
let i = 0;
while (true) {
yield String.fromCharCode(
97 + (i >= 26 ? i = 0 : i++)
);
}
}
const alphabet = loopingAlphabet();
alphabet.next(); // => { value: "a" }
alphabet.next(); // => { value: "b" }
alphabet.next(); // => { value: "c" }
// ...
alphabet.next(); // => { value: "z" }
alphabet.next(); // => { value: "a" }
alphabet.next(); // => { value: "b" }
// ...
Such infinite applications of generators are rare but they do exist and are a perfect place to use while(...). Most other applications of while have been replaced with more succinct and contained methods of iteration such as for...in and for...of. Nonetheless, it is useful to know how to cleanly wield it.
The do...while statement is similar to while it although guarantees an iteration before the check is carried out. Its syntax is formed of the do keyword followed by its body and then a typical parenthesized while expression:
do IterationBody while (ConditionExpression)
The purpose of each part is as follows:
Although the behavior of the do...while statement is different from regular while statement, its semantics and broad applications remain the same. It is most useful in contexts where you need to always complete at least one step of an iteration before either checking whether to continue or changing the subject of the iteration. An example of this would be upward DOM traversal. If you have a DOM element and wish to run certain code on it and each of its DOM ancestors, then you may wish to use a do...while statement as follows:
do {
// Do something with `element`
} while (element = element.parentNode);
A loop like this will execute its body once for the element value, whatever element is, and then will evaluate the assignment expression, element = element.parentNode. This assignment expression will evaluate to its newly assigned value, meaning that, in the case of element.parentNode being falsy (for example, null) the do...while will halt its iteration.
Assigning values in the ConditionExpression portion of a while or do...while statement is relatively common although it can be obscure to fellow programmers, so it's best to only do so if it's plainly obvious what the intent of the code is. If the preceding code was wrapped in a function called traverseDOMAncestors, then that would provide a helpful clue.
The switch statement is used to move control to a specific inner case clause that specifies a value that matches the value passed to switch(...). It has the following syntax:
switch (SwitchExpression) SwitchBody
SwitchExpression will be evaluated once and its value compared via strict-equality to case statements within SwitchBody. Within SwitchBody there may be one or more case clauses and/or a default clause. The case clauses designate CaseExpression, whose value will be compared to that of SwitchExpression, and their syntax is as follows:
case CaseExpression:
[other JavaScript statements or additional clauses]
The switch statement is usually used to specify a selection of two or more mutually exclusive outcomes based on a specific value. With fewer conditions, it'd be conventional to use an if...else construct, but to accommodate more potential conditions, it's simpler to use switch:
function generateWelcomeMessage(language) {
let welcomeMessage;
switch (language) {
case 'DE':
welcomeMessage = 'Willkommen!';
break;
case 'FR':
welcomeMessage = 'Bienvenue!';
break;
default:
welcomeMessage = 'Welcome!';
}
return welcomeMessage;
}
generateWelcomeMessage('DE'); // => "Willkommen!"
generateWelcomeMessage('FR'); // => "Bienvenue!"
generateWelcomeMessage('EN'); // => "Welcome!"
generateWelcomeMessage(null); // => "Welcome!"
Once the switch mechanism finds the appropriate case, it will execute all code following that case statement until the very end of the switch statement or until it encounters a break statement. A break statement is used to break out of SwitchBody when the desired work is accomplished.
Given that switch statements are usually used to execute specific and mutually exclusive pieces of code depending on the value, it is conventional to use break between every case statement to ensure that only the appropriate code runs for any given value. Sometimes, however, it is desirable to avoid breaking between cases and let the SwitchBody code continue to run through multiple case statements and beyond. Doing this is known as fallthrough:
switch (language) {
case 'German':
case 'Deutsche':
case 'DE':
welcomeMessage = 'Willkommen!';
break;
case 'French':
case: 'Francais':
case 'FR':
welcomeMessage = 'Bienvenue!';
break;
default:
welcomeMessage = 'Welcome!';
}
Here, you can see that we are employing fallthrough so that a language of either 'German', 'Deutsche', or 'DE' will result in the same code running welcomeMessage = 'Willkommen!'. And following that, we immediately break to prevent any more of SwitchBody from running.
When you have a switch statement residing in a function, it is sometimes best to simply return the intended values instead of having to rely on break statements. For example, in generateWelcomeMessage, we can simply return the welcome string. There's no need to go through the rigamarole of initializing a variable, assigning it, and breaking between cases:
function generateWelcomeMessage(language) {
switch (language) {
case 'DE':
return 'Willkommen!';
case 'FR':
return 'Bienvenue!';
default:
return 'Welcome!';
}
}
Returning directly, in this way, is arguably clearer than breaking within each case, especially if each case's logic is fairly simple.
Usually, the code following a case or default clause will not only occupy a single line. As such, it has become conventional to wrap these statements with a block, so that there is a sense of containment:
switch (speed) {
case 'slow': {
console.log('Initiating slow speed');
car.changeSpeedTo(speed);
car.enableUrbanCollisionControl();
}
case 'fast': {
console.log('Initiating fast speed');
car.changeSpeedTo(speed);
car.enableSpeedLimitWarnings();
car.enableCruiseControlOption();
}
case 'regular':
default: {
console.log('Initiating regular speed');
car.changeSpeedTo(speed);
}
}
This isn't strictly necessary and doesn't change any functionality, but it does offer more clarity to the reader of our code. It also paves the way for any block-level variables, should we wish to introduce these later. As we know, within a block (delimited with { and }), we can use const and let to declare variables that will be scoped only to that block:
switch (month) {
case 'December':
case 'January':
case 'February': {
const message = 'In the UK, Spring is coming soon!';
// ...
}
//...
}
Here, we're able to declare specific variables that are scoped to the February case only. This is useful if we have a large amount of logic that we'd like to isolate. At this point, however, we should consider abstracting that logic in some other way. Lengthy switch statements can be incredibly hard to understand.
Often, there's a need to express more complex conditions in each case, instead of just matching a singular value. If we pass true as SwitchExpression, then we are free to express custom conditional logic within each CaseExpression, as long as each CaseExpression evaluates to true when successful:
switch (true) {
case user.role === 'admin' || user.role === 'root': {
// ...
break;
}
case user.role === 'member' && user.isActive: {
// ...
break;
}
case user.role === 'member' && user.isRecentlyInactive: {
// ...
break;
}
}
This pattern allows us to express more multivariate and hybrid conditions. You may usually feel inclined toward multiple if/else/if/else statements, but if your logic can be expressed in a switch statement, then it may be best to opt for that. As always, you should consider the nature of your problem domain and its logic, and seek to make an informed decision about how you wish to implement your control flow. In some cases, a switch statement will only end up being more confusing.
In the next section, we will cover some other approaches you can use to handle complex and lengthy logic that doesn't suit native constructs such as switch.
Cyclomatic complexity is a measure of how many linearly independent paths there are through a program's code.
Consider a simple program that contains several conditional checks and function invocations:
if (a) {
alpha();
if (b) bravo();
if (c) charlie();
}
if (d) delta();
Even in this misleadingly simple piece of code, nine distinct paths can be taken. So, depending on the values of a, b, c, and d, there are nine possible sequences of alpha, bravo, charlie, and delta that will run:
A high level of cyclomatic complexity is undesirable. It can lead to the following:
It's frustratingly easy to end up in a situation where there are so many different conditions and branches within a singular module that nobody can understand what's happening. In addition to using tools to help us to identify areas of high complexity, we can use our own judgment and intuitions. The following are some examples of complexity that we can easily identify and avoid:
These are not precise cautions but they should give you an idea of what you should watch out for. When we find such complexity, the first thing we should do is to sit back and re-consider our problem domain. Can we describe our logic differently? Can we form new or different abstractions?
Let's explore an example of a piece of code with high cyclomatic complexity and consider how we might simplify it with these questions in mind.
To illustrate too much cyclomatic complexity and how we should approach simplifying it, we're going to be refactoring a piece of code that is responsible for deriving a set of ID numbers and types from a set of licenses:
function getIDsFromLicenses(licenses) {
const ids = [];
for (let i = 0; i < licenses.length; i++) {
let license = licenses[i];
if (license.id != null) {
if (license.id.indexOf('c') === 0) {
let nID = Number(license.id.slice(1));
if (nID >= 1000000) {
ids.push({ type: 'car', digits: nID });
} else {
ids.push({ type: 'car_old', digits: nID });
}
} else if (license.id.indexOf('h') === 0) {
ids.push({
type: 'hgv',
digits: Number(license.id.slice(1))
});
} else if (license.id.indexOf('m') === 0) {
ids.push({
type: 'motorcycle',
digits: Number(license.id.slice(1))
});
}
}
}
return ids;
}
This function accepts an array of licenses and then extracts the ID numbers of those licenses (avoiding cases of null or undefined IDs). We determine the type of license based on characters found within its ID. There are four types of licenses that need to be identified and extracted:
The following is an example of the input and the derived output of the getIDsFromLicenses function:
getIDsFromLicenses([
{ name: 'Jon Smith', id: 'c32948' },
{ name: 'Marsha Brown' },
{ name: 'Leah Oak', id: 'h109' },
{ name: 'Jim Royle', id: 'c29283928' }
]);
// Outputs:
[
{type: "car_old", digits: 32948}
{type: "hgv", digits: 109}
{type: "car", digits: 29283928}
]
As you may have observed, the code we've used to extract the IDs is quite cyclomatically complex. You may consider it perfectly reasonable code, and it arguably is, but it could be simpler still. Our function achieves its results imperatively, using up a lot of syntax to explain how it wants to accomplish its task instead of what it wants to accomplish.
To simplify our code, it's first useful to take a fresh look at the problem domain. The task we want to accomplish is to take an input array and, from it, derive a set of license ID types and values. The output array will be an almost 1:1 mapping from the input array, except for cases where licenses have a falsy id property (null, in this case). The following is an illustration of our I/O flow:
[INPUT LICENSES] ==> (DERIVATION LOGIC) ==> [OUTPUT ID TYPES AND DIGITS]
Looked at abstractly in this way, this seems like the perfect opportunity to use Array#map. The map method allows us to run a function on every element within an array to derive a new array containing mapped values.
The first thing we'll want to map is the license to its id:
ids = licenses.map(license => license.id)
We'll want to handle cases where there is no id. To do this, we can apply a filter on the derived IDs:
ids = ids.filter(id => id != null)
And, in fact, because we know that all valid IDs are truthy, we can simply do a Boolean check by directly passing Boolean as our filter function:
ids = ids.filter(Boolean)
From this, we'll receive an array of our licenses but only those with a truthy id property. Following this, we can consider the next transformation we wish to apply to the data. We'd like to split the id value into its constituent parts: we need the initial character of the ID (id.charAt(0)), and then we want to extract the remaining characters (the digits) and cast them to the Number type (Number(id.slice(1))). We can then pass these parts to another function, which will be responsible for extracting the correct ID fields (type and digits) from this information:
ids = ids.map(id => getIDFields(
id.charAt(0),
Number(id.slice(1))
));
The getIDFields function will need to determine the type from the individual character and digits for the ID, returning an object of the { type, digits } form:
function getIDFields(idType, digits) {
switch (idType) {
case 'c': return {
type: digits >= 1000000 ? 'car' : 'car_old',
digits
};
case 'h': return { type: 'hgv', digits };
case 'm': return { type: 'motorcycle', digits };
}
}
Since we've abstracted this part our logic away to an individual function, we can independently observe and test its behavior:
getIDFields('c', 1000); // => { type: "car_old", digits: 1000 }
getIDFields('c', 2000000); // => { type: "car", digits: 1000 }
getIDFields('h', 1000); // => { type: "hgv", digits: 1000 }
getIDFields('i', 1000); // => { type: "motorcycle", digits: 1000 }
Tying everything together, we end up with a new implementation of getIDsFromLicenses that looks like this:
function getIDsFromLicenses(licenses) {
return licenses
.map(license => license.id)
.filter(Boolean)
.map(id => getIDFields(
id.charAt(0),
Number(id.slice(1))
))
}
What we have achieved here is a significant reduction in the amount of cyclomatic complexity that our fellow programmers will need to contend with. We are utilizing Array#map and Array#filter to abstract away both decision-making and iteration logic. This means we end up with an implementation that is far more declarative.
You may notice, as well, that we have extracted repeated logic and generalized it. For example, in our initial implementation, we were implementing many calls to discover the first character of the ID (for example, license.id.indexOf('m') === 0). Our new implementation generalizes this by mapping to a data structure that already includes the first character as a distinct value that we can then pass through to getIDFields to get the relevant type and digits for that ID.
To summarize, our general refactoring approach has involved the following considerations:
Our code is now easier to comprehend, and hence easier to maintain and debug. It'll likely also be more reliable and stable since its individual units can be more simply tested and can hence avoid regressions in the future. There is, naturally, the potential for a slight performance decrease due to the increased usage of higher abstracted declarative idioms and functions over imperative code, but this is an incredibly marginal difference and, in the vast majority of situations, is worth implementing for the significant benefits that the refactoring produces in terms of maintainability and reliability.
Most of the constructs we've looked at so far are used for synchronous code, where statements are evaluated sequentially, with each line completing before the next one begins:
const someValue = getSomeValue();
doSomethingWithTheValue(someValue);
Code like this is straightforward. We intuitively understand that these two lines of code will run one after the other. There is also an assumption that neither of these lines will take very long to execute, probably taking no more than a few micro- or milliseconds.
But what happens if we wish to bind to a user Event or fetch some remote data? These are things that take time and will only complete when some future Event occurs. In a less kind universe, there would be no way to deal with such scenarios other than simply waiting for them to complete and then continuing the execution of our program:
fetchSomeData();
processFetchedData();
In this unkind universe, fetchSomeData() would be a blocking function call, so named because it would block the execution of all other code until it finally completes. This means that we wouldn't be able to carry out any other vital tasks, and our application would essentially be at a standstill state until the task is completed, negatively affecting the user experience.
Thankfully, JavaScript gives us a nicer universe than this—one in which we can initialize a task, such as fetching data, and then continue on with the rest of our program while that task is running. Such tasks are named asynchronous because they occur and complete non-synchronously, at a later time than now. When they do finally complete, JavaScript can helpfully notify us of this fact, calling whatever code depends upon the completion of that task.
To accomplish this, JavaScript maintains a single-threaded Event Loop. When the Event Loop kicks off, it'll run our program. Following the execution of a piece of code (such as that which initiates our program), the Event Loop will await messages (or Events) indicating that something has occurred (for example, a network request has completed or a browser UI event has occurred). When it receives a message, it will then execute any code that is depending upon or listening for that Event. The Event Loop will, again, run that code to completion before continuing to await other messages. This process repeats infinitely until the JavaScript program is halted (for example, by closing a tab in a browser).
The fact that the Event Loop will always run a given piece of code to its completion means that any long-running or blocking code will prevent any other code from executing until it has completed. Some older browser API methods such as alert() and prompt() are examples of blocking functions that you may encounter. Calling these will effectively block any further execution of your JavaScript program:
alert('Hello!');
console.log('The alert has been dismissed by the user');
Here, console.log() will not be evaluated until the alert dialog is dismissed by the user. This could be milliseconds, minutes, or even hours. During this period, our JavaScript program is halted, unable to continue. Its Event Loop may be receiving Events but it will not run the code associated with those Events until alert() finally completes.
Nowadays, it's normal to expect APIs within a browser and server to provide non-blocking asynchronous ways to call native mechanisms. Common examples of such APIs include the following:
All such interfaces share something in common: they all provide a way to somehow listen for their completion. Usually, this is achieved via a provided callback (a function). This callback will be called at some later point when the task has completed. Similarly, some native APIs return promises, which enable a richer mechanism of asynchronous control flow, but fundamentally still rely on passing callbacks via the Promise API. Additionally, ECMAScript 2017 introduced the concept of asynchronous functions (async function() {}) and the await keyword, which finally provided language support for promises, meaning that the completion of asynchronous work no longer requires callbacks.
Let's explore each of these asynchronous of control flow mechanisms individually.
A callback is a conventional approach to providing a way to hook into asynchronous tasks. A callback is simply a function that is passed to another function and is expected to be called at some later point, possibly immediately, possibility soon, and possibly never. Consider the following requestData function:
function requestData(path, callback) {
// (Implementation of requestData)
}
As you can see, it accepts a callback as its second argument. When calling requestData, the callback will typically be anonymously passed inline, like so:
requestData('/data/123', (response) => { /* ... */ });
It is, of course, totally fine to have previously declared the callback, and doing so can aid comprehensibility as now the reader of your code will have an inkling as to when a callback might be invoked. Observe here how we're calling our onResponse callback to make clear that it is expected to be called upon the response becoming available (when it completes):
function onResponse(response) {
// Do something with the response...
}
requestData('/data/123', onResponse);
Similarly, in complex APIs with multiple asynchronous state changes, it's common to see named callbacks registered in bulk, via an object literal:
createDropdownComponent({
onOpen() {},
onSelect() {},
onClose() {},
onHover() {} // etc.
});
A callback will typically be passed arguments that indicate some important state that has been determined from the asynchronous work. For example, the Node.js readFile function invokes its callback with two arguments, a (possibly null) error and the (possibly null) data from the file itself:
fs.readFile('/path/to/file', (error, data) => {
if (error) {
// Handle the error!
} else {
// Handle the data! (No error has occurred!)
}
});
The function you pass a callback to is entirely in control of when your callback is invoked, how it is invoked, and what data is passed along with that invocation. This is why sometimes callbacks are spoken about as an inversion of control. Normally, you are in control of what functions you call, but when using callbacks, the control is inverted so that you are relying on another function or abstraction to (at some point) call your callback in the expected manner.
Callback hell is the name given to the undesirable proliferation of multiple nested callbacks within a piece of code, usually done to carry out a series of asynchronous tasks that each rely on another previous asynchronous task. Here is an example of such a situation:
requestData('/data/current-user', (userData) => {
if (userData.preferences.twitterEnabled) {
requestData(userData.twitterFeedURL, (twitterFeedData) => {
renderTwitterFeed(twitterFeedData, {
onRendered() {
logEvent('twitterFeedRender', { userId: userData.id });
}
});
});
}
});
Here, you can see we have three different callbacks, all appearing in one hierarchy of scopes. We await the response of /data/current-user, then we optionally make a request to twitterFeedURL, and then, upon the rendering of the twitter feed (renderTwitterFeed()), we finally log a "twitterFeedRender" Event. That final log depends on two previous asynchronous tasks completing and so is (seemingly unavoidably) nested quite deeply.
We can observe that this deeply nested piece of code is at the peak of a kind of horizontal pyramid of indentation. This is a common trait of callback hell, and as such, you can use the existence of these horizontal pyramids as something to watch out for. Not all deep indentations will be due to callbacks, of course, but it's usually high on the list of suspects:

To avoid the callback hell indicated by the horizontal pyramid, we should consider re-thinking and potentially re-abstracting our code. In the preceding case, logging a Twitter feed render Event, we could, for example, have a generalized function for getting and rendering Twitter feed data. This would simplify the top-level of our program:
requestData('/data/current-user', (userData) => {
if (userData.preferences.twitterEnabled) {
renderTwitterForUser(userData);
}
});
Observe how we have shortened our horizontal pyramid here. We are now free to implement renderTwitterForUser as we wish and import it as a dependency. Even though its implementation may involve its own callbacks, it is still a reduction in overall complexity for the programmer as it abstracts away half of the pyramid to a neatly separated abstraction. Most callback hell scenarios can be solved with a similar approach to re-designing and abstraction. This was a simple scenario, though. With more intertwined asynchronous tasks, it may be useful to use other mechanisms of asynchronous control flow.
JavaScript is a language that feels right at home when subscribing to and emitting Events. Events are incredibly common in most JavaScript programs, whether dealing with user-derived Events within the browser or server-side Events in Node.js.
There are various names used for operations relating to Events in JavaScript, so it's useful to know all of these names upfront so we're not confused when encountering them. An event is an occurrence in time that will result in the invocation of any callbacks that have been subscribed for that Event. Subscribing to an Event has many names, which all effectively mean the same thing: subscribing, registering, listening, binding, and so on. When the Event occurs, the subscribed callback is invoked. This, as well, has many names: invoking, calling, emitting, firing, or triggering. The actual function that is called can also have various names: function, callback, listener, or handler.
At its core, any abstraction that supports Events will usually do so by storing callbacks to be called later, keyed with specific Event names. We can imagine that a DOM element might store its Event listeners in a structure like the following:
{
"click": [Function, Function, Function],
"mouseover": [Function, Function],
"mouseout": [Function]
}
Any Event-supporting abstraction will simply store a series of callbacks to be called later. As such, when subscribing to an Event, you will need to provide both the callback you wish it to call and the Event name that it will be tied to. In the DOM, we would do this like so:
document,body.addEventListener('mousemove', e => {
e; // => the Event object
});
Here, we see that an Event object is passed to the callback. This is idiomatically named e or evt for succinctness. Most abstractions that provide an Events API will pass specific Event-related information to the callback. This may be in the form of a singular Event object or several arguments.
It's important to note that there truly is no single standard for Events although there are conventions that have emerged. Typically there will always be a method used to register or subscribe to an Event and then another to remove that subscription. The following is an example of using the Node.js Event-Emitter API, which is supported by the native HTTP module:
const server = http.createServer(...);
function onConnect(req, cltSocket, head) {
// Connect to an origin server...
}
// Subscribe
server.on('connect', onConnect);
// Unsubscribe
server.off('connect', onConnect);
Here, you can see that the on() method is used to subscribe to Events, and the off() method is used to unsubscribe. Most Events APIs have similar event registration and de-registration methods although they may implement them in different ways. If you're crafting your own Events implementation, then it's advisable to ensure that you're providing a familiar set of methods and abstractions. To do this, take inspiration from either the native DOM Events interface or the Node.js Event Emitter. This will ensure that your Events implementation does not surprise or horrify other programmers too much.
Even though an Events API is essentially just a series of callbacks stored and invoked at specific times, there are still challenges in crafting it well. Amongst them are the following:
Events provide more possibilities than a simple callback. Since they allow several different Events to be listened for, and the same Event to be listened for several times, any consuming code has far more flexibility in how it constructs its asynchronous control flow. An object that has an Events interface can be passed around throughout a code base and may be subscribed to many times, potentially. The nature of distinct Events, as well, means that different asynchronous concepts or occurrences are usefully kept separated so that a fellow programmer can easily tell which action will be taken in specific circumstances:
const dropdown = new DropDown();
dropdown.on('select', () => { /*...*/ });
dropdown.on('deselect', () => { /*...*/ });
dropdown.on('hover', () => { /*...*/ });
This type of transparent separation helps to encode expectations within the mind of the programmer. It's simple to discern which function will be called in each case. Compare this to a generalized something happened event with an internal switch statement:
// Less transparent & more burdensome:
dropdown.on('action', event => {
switch (event.action) {
case 'select': /*...*/; break;
case 'deselect': /*...*/; break;
// ...
}
});
Well-implemented Events provide a good semantic separation between conceptually different Events and, therefore, provide the programmer with a predictable series of asynchronous actions that they can reason about easily.
A Promise is an abstraction that surrounds the concept of an eventual value. It's easiest to think of a Promise as a simple object that will, at some point, contain a value. A Promise provides an interface via which you can pass callbacks to wait for either the eventually-fulfilled value or an error.
At any given time a Promise will have a certain state:
Promises can be constructed via the Promise constructor, by passing a singular function argument (called an executor) that calls either a resolve or reject function to indicate either a settled value or an error, respectively:
const answerToEverything = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(42);
}, 1000);
});
The instantiated Promise has the following methods available so that we can access its changed state (when it moves from pending to either fulfilled or rejected):
We can access the eventually resolved value of answerToEverything by passing a callback to its then method:
answerToEverything.then(answer => {
answer; // => 42
});
We can illustrate the exact nature of a Promise by exploring the native Fetch API, supported by most modern browsers:
const promiseOfData = fetch('/some/data?foo=bar');
The fetch function returns a Promise that we assign to our variable, promiseOfData. We can then hook into the request's eventual success (or failure) like so:
const promiseOfData = fetch('/some/data');
promiseOfData.then(
response => {
response; // The "fulfilled" Response
},
error => {
error; // The "rejected" Error
}
);
It may appear as though promises are just a slightly more verbose abstraction than callbacks. Indeed, in the simplest case, you might just pass a fulfillment callback and a rejection callback. This, arguably, does not provide us with anything more useful than the original callback approach. But promises can be so much more than this.
Since a Promise is just a regular object, it can be passed around your program just like any other value, meaning that the eventual resolution of a task no longer needs to be tied to code at the call site of the original task. Additionally, the fact that each then, catch, or finally call returns a Promise of its own, we can chain together any number of either synchronous or asynchronous tasks that rely on some original fulfillment.
In the case of fetch(), for example, the fulfilled Response object provides a json() method, which itself completes asynchronously and returns a Promise. Hence, to get the actual JSON data from a given resource, you would have to do the following:
fetch('/data/users')
.then(response => response.json())
.then(jsonDataOfUsers => {
jsonDataOfUsers; // the JSON data that we got from response.json()
});
Chaining together then calls is a popular pattern used to derive a new value from some prior value. Given the response, we wish to compute the JSON, and given the JSON, we may wish to compute something else:
fetch('/data/users')
.then(response => response.json())
.then(users => users.map(user => user.forename))
.then(userForenames => userForenames.sort());
Here, we are using multiple then calls to compute the sorted forenames of our users. There are, in fact, four distinct promises being created here, as foll:
const promiseA = fetch('/data/users');
const promiseB = promiseA.then(response => response.json());
const promiseC = promiseB.then(users => users.map(user => user.forename))
const promiseD = promiseC.then(userForenames => userForenames.sort());
promiseA === promiseB; // => false
promiseB === promiseC; // => false
promiseC === promiseD; // => false
Each Promise will only ever resolve to a single value. Once it's been either fulfilled or rejected, no other value can take its place. But as we see here, we can freely derive a new Promise from an original Promise by simply registering a callback via then, catch, or finally. The nature of only resolving once and of returning new derived promises means that we can compose promises together in a number of useful ways. In our example, we could derive two promises from our users data Promise: one that collects the forenames of users and another that collects their surnames:
const users = fetch('/data/users').then(r => r.json());
const forenames = users.then(users => users.map(user => user.forename));
const surnames = users.then(users => users.map(user => user.surname));
We can then freely pass around these forenames and surnames promises, and any consuming code can do what it wants with them. For example, we may have a DOM element that we'd like to populate with the forenames when they are eventually available:
function createForenamesComponent(forenamesPromise) {
const div = document.createElement('div');
function render(forenames) {
div.textContent = forenames ? forenames.join(', ') : 'Loading...';
}
render(null); // Initial render
forenamesPromise.then(forenames => {
// When we receive the forenames we want to render them:
render(forenames);
});
return div;
}
This createForenamesComponent function accepts the forenames Promise as an argument and then returns a <div> element. As you can see, we have called render() initially with null, which populates the DIV element with the "loading..." text. Once the Promise is fulfilled, we then re-render with the newly populated forenames.
The ability to pass around promises in this manner makes them far more flexible than callbacks, and similar in spirit to an object that implements an Events API. However, with all of these mechanisms, it is necessary to create and pass around functions so that you can listen for future Events and then act on them. If you have a significant amount of asynchronous logic to express, this can be a real struggle. The control flow of a program littered with callbacks, Events, and promises can be unclear, even to those well accustomed to a particular code base. Even a small number of independently asynchronous Events can create a large variety of states throughout your application. A programmer can become very confused, as a result; the confusion relates to what is happening when.
To avoid confusion, it's best to implement any timing-related code as transparently as possible, so that there is no room for misunderstanding. The following is an example of code that may lead to misunderstanding:
userInfoLoader.init();
appStartup().then(() => {
const userID = userInfoLoader.data.id;
const userName = userInfoLoader.data.name;
renderApplication(userID, userName);
});
This code seems to assume that the Promise returned by appStartup() will always fulfill after userInfoLoader has completed its work. Perhaps the author of this code happens to know that the appStartup() logic will always complete after userInfoLoader. Perhaps that is a certainty. But for us, reading this code for the first time, we have no confidence that userInfoLoader.data will be populated by the time appStartup() is fulfilled. It would be better to make the timing more transparent by, for example, returning a Promise from userInfoLoader.init() and then carrying out appStartup() on the explicit fulfillment of that Promise:
userInfoLoader.init()
.then(() => appStartup())
.then(() => {
const userID = userInfoLoader.data.id;
const userName = userInfoLoader.data.name;
renderApplication(userID, userName);
});
Here, we are arranging our code so that it is obvious what actions are dependent on what other actions and in what order the actions will occur. Using promises by themselves, just like any other asynchronous control flow abstraction, does not guarantee that your code will be easily comprehensible. It's important to always consider the perspective of your fellow programmers and the temporal assumptions that they'll make. Next, we will explore a newer addition to JavaScript that gives us native linguistic support for asynchronous code: you'll see how these additions enable us to write asynchronous code that is clearer in terms of what is happening when.
The ECMAScript 2017 specification introduced new concepts to the JavaScript language in the form of the async and await keywords. The async keyword is used to designate a function as asynchronous:
async function getNumber() {
return 42;
}
Doing this, effectively, wraps whatever the function returns in Promise (if it is not already Promise). So, if we attempt to call this function we will receive Promise:
getNumber() instanceof Promise; // => true
As we've learned, we can subscribe to the fulfillment of Promise by using its then method:
getNumber().then(number => {
number; // => 42
});
In concert with async functions that return Promises, we also have an await keyword. This enables us to wait for the fulfillment (or rejection) of the Promise simply by passing it to the right side of await. This may, for example, be a Promise returned from an async function call:
await someAsyncFunction();
Or it may be a Promise designated inline, like so:
const n = await new Promise(fulfill => fulfill(123));
n; // => 123
As you can see, the await keyword will wait for its Promise to resolve and thereby prevents any following lines from executing until that occurs.
The following is another example—a setupFeed async function that awaits both fetch() and response.json():
async function setupFeed() {
const response = await fetch('/data');
const json = await response.json();
console.log(json);
}
It's important to note that the await keyword does not block like alert() or prompt(). Instead, it simply pauses the execution of the asynchronous function, freeing up the Event Loop to continue with other work, and then, when its Promise resolves, it will continue execution where it left off. Remember, await is only syntactic sugar over functionality that we can already achieve. If we wanted to implement our setupFeed function without async/await, we could easily do that by reverting to our old pattern of passing callbacks to Promise#then:
function setupFeed() {
fetch('/data').then(response => {
return response.json()
}).then(json => {
console.log(json);
});
}
Observe how the code is slightly clunkier and more congested when we don't use await. Using await in concert with asynchronous functions gives us the same satisfyingly linear and procedural appearance as regular synchronous code. This can vastly simplify an otherwise complicated asynchronous control flow, making it clearer to our fellow programmer what is happening when.
The await keyword is also available for use within the for...of iteration construct. Doing so will await each value iterated over. If, during iteration, any encountered value is a Promise, the iteration will not continue until that Promise has been resolved:
const allData = [
fetch('/data/1').then(r => r.json()),
fetch('/data/2').then(r => r.json()),
fetch('/data/3').then(r => r.json())
];
for await (const data of allData) {
console.log(data);
}
// Logs data from /data/1, /data/2 and /data/3
Without Promises or await and async, expressing this kind of asynchronous process would require not only also more code but also more time to understand. The beauty of these constructs and abstractions is that they allow us to ignore the implementation details of asynchronous operations, enabling us to focus purely on expressing our problem domain. As we move forward in this book, we will further explore this spirit of abstraction as we tackle some larger and more unwieldy problem domains.
In this chapter, we have finalized our exploration of the JavaScript language, discussing the difference between imperative and declarative syntax, exploring how to cleanly control flow, and learning how to handle cases of cyclomatic complexity in both synchronous and asynchronous contexts. This has involved an in-depth study of all iteration and conditional constructs within the language, guidance on their usage, and cautions against anti-patterns.
In the next chapter, we will take all of the knowledge we've accrued about the JavaScript language and combine it with some explorations into real-world design patterns and paradigms that'll help us to build clean abstractions and architectures.
In this section, we'll take what we've learned about clean code and JavaScript's language constructs and apply this in order to build clean and coherent JavaScript abstractions. By doing this, we'll learn how to design intuitive abstractions using well-known patterns, how to think about common JavaScript problem domains, how to deal with error states, and how to work effectively with the sometimes awkward DOM API.
This section contains the following chapters:
Most problems we encounter are not new. Many programmers that have come before us have tackled similar problems and, via their struggles, various patterns of programming have emerged. We call these design patterns.
Design patterns are the useful structures, styles, and stencils that our code sits within. A design pattern may prescribe anything from the overall scaffolding of a code base to the individual syntactic pieces used to build expressions, functions, and modules. By building software, we are constantly, and often unknowingly, in the process of designing. It is through this process of designing that we are defining the experience that users and maintainers will go through when exposed to our code.
To attune us to this perspective of the designer instead of programmer, for a moment, let's consider the design of a simple software abstraction.
In this chapter, we will cover the following topics:
To bestow us with the perspective of a designer, let's explore a simple problem. We must construct an abstraction that allows users to give us two strings, a subject string and a query string. We must then calculate a count of the query strings found within the subject string.
So, consider the following query string:
"the"
And have a look at the following subject string:
"the fox jumped over the lazy brown dog"
We should receive a result of 2.
For our purposes as a designer, we care about the experience of those who must use our code. For now, we won't worry about our implementation; we will instead only consider the interface, as it is primarily the interface to our code that will drive our fellow programmers' experiences.
The very first thing we may do as a designer is to define a function with a carefully chosen name and a specific set of named arguments:
function countNeedlesInHaystack(needle, haystack) { }
This function accepts needle and haystack and will return Number, indicating the count of needle within haystack. The consumer of our code would make use of it like so:
countNeedlesInHaystack('abc', 'abc abc abc'); // => 3
The design of a piece of code should be defined by the problem domain we wish to solve and the user experience we wish to reveal. Another programmer, given the same problem domain, may have chosen a different solution. For example, they may have employed partial application to allow the following calling syntax:
needleCounter('app')('apple apple'); // => 2
Or perhaps they may have designed a more verbose syntax that involves invoking a Haystack constructor and calling its count() method like so:
new Haystack('apple apple'),count('app'); // => 2
This classical approach arguably has a nice semantic relationship between the object (Haystack) and the count method. It meshes well with the OOP concepts we've explored in previous chapters. That said, some programmers may find it to be overly verbose.
There's also the possibility of a more descriptive API where the arguments are defined within a configuration object (that is, a plain object literal passed as the sole argument):
countOccurancesOfNeedleInHaystack({
haystack: 'abc abc abc',
needle: 'abc'
}); // => 3
There's also the possibility that this counting functionality may form the part of a larger set of string-related utilities and, hence, can be incorporated into a larger custom-named module:
str('omg omg omg').count('omg'); // => 3
We may even consider it okay to modify the native String.prototype, even though it is inadvisable, so that we have a count method available on all strings:
'omg omg omg'.count('omg'); // => 3
In terms of our naming conventions as well, we may wish to avoid the needle-in-a-haystack idiom and, instead, use more descriptive names where perhaps there is less risk of misunderstanding, like the following:
The choices available to us, even within this very narrow problem domain, are overwhelming. You'll likely have many of your own strong opinions about which approach and naming conventions would have been superior here.
The fact that we can solve a seemingly simple problem with so many different approaches shows us how there is a need for a decision process. And this process is software design. Effective software design employs design patterns to encapsulate problem domains and provide familiarity and ease of comprehension to fellow programmers.
A well-chosen design pattern, given any problem domain, can be said to have two basic characteristics:
Design patterns are useful in a variety of contexts and scales. We use them when we write individual operations and functions, but we also use them when structuring our entire code base. Design patterns, as such, are hierarchical. They exist on the macro and micro scale of a code base. A singular code base can easily contain many design pattern within.
In Chapter 2, Tenets of Clean Code, we spoke about familiarity as a crucial characteristic. A car mechanic opening the hood of a car will hope to see many familiar patterns: from the individual pieces of wiring and welding of Components to the larger construction of the cylinders, valves, and pistons. There is a certain layout they would expect to find and if it is not there, then they would be left scratching their heads, wondering how to approach whatever problem they're trying to solve.
Familiarity increases the maintainability and usability of our solutions. Consider the following directory structure and the displayed logger.js source code:

What design patterns can we observe here? Let's take a look at some examples:
Design patterns are not just large, lofty architectural structures. They can exist in every part of our code base: the directory structure, the naming of files, and the individual expressions of our code. At every level, our usage of common patterns can increase our ability to express the problem domain, and increase the familiarity of our code to newcomers. Patterns exist within patterns.
Using design patterns well can have beneficial effects on all of the tenets of clean code we covered previously—reliability, efficiency, maintainability, and usability:
You can see that a lot of what makes design patterns useful is only actualized if we pick the right one. We'll be exploring a selection of popular design patterns and, for each, we'll discuss the types of situations they're suited to. This exploration should hopefully give you a good idea of what it means to select a good design pattern.
Architectural design patterns are the ways in which we tie our code together. If we have a dozen different modules, it is how those modules talk to each other that defines our architecture.
The architectural design patterns utilized in JavaScript code bases have changed massively over recent years. With the steady proliferation of popular frameworks such as React and Angular, we've seen code bases take on new conventions. The landscape is still very much shifting, so we shouldn't expect any specific standard to emerge any time soon. Nonetheless, most frameworks tend to follow the same broad architectural patterns.
In this section, we'll be covering two famous architectural design patterns, MVC and its offshoot, Model-View-ViewModel (MVVM). Together, these should give us an awareness of the types of concerns that are typically separated and will hopefully inspire us to seek a similar level of clarity in the architectures we create.
MVC is characterized by a separation between these three concepts. An MVC architecture may involve many individual Models, Views, and Controllers that all work in concert to solve a given problem. Each of these parts can be described as follows:
We can observe the flow of control in the following diagram:

The MVC pattern provides us with a way to separate our various concerns. It prescribes where we should put logic about business decisions (that is, in Models) and where we should put logic about displaying things to the user (that is, Views). Additionally, it gives us the Controller, which enables these two concerns to talk to each other. The separation that MVC fosters is hugely beneficial as it means our fellow programmers can easily discern where to make required changes or fixes.
To illustrate how an implementation of MVC might look in JavaScript, let's build a very simple program. It will be a basic mutable number application that renders a simple UI where the user can see the current number and choose to update it via either incrementing or decrementing its value.
First, we can implement the logic and containment of our data using a Model:
class MutableNumberModel {
constructor(value) {
this.value = value;
}
increment() {
this.value++;
this.onChangeCallback();
}
decrement() {
this.value--;
this.onChangeCallback();
}
registerChangeCallback(onChangeCallback) {
this.onChangeCallback = onChangeCallback;
}
}
In addition to storing the value itself, this class also accepts and relies upon a callback function called onChangeCallback. This callback function will be provided by the Controller and will be called whenever the value changes. This is necessary so that we can kick off a re-render of the View if the Model changes.
Next, we need to build the Controller, which will act as a very simple bridge (or glue) between view and model. It registers the necessary callbacks to know when either the user requests a change via view or the underlying data of the model changes:
class MutableNumberController {
constructor(model, view) {
this.model = model;
this.view = view;
this.model.registerChangeCallback(
() => this.view.renderUpdate()
);
this.view.registerIncrementCallback(
() => this.model.increment()
);
this.view.registerDecrementCallback(
() => this.model.decrement()
);
}
}
Our view is responsible for retrieving data from model and rendering it to the user. To do this, it creates a DOM hierarchy in which the data will sit. It also listens for and escalates user events to controller when either the increment or decrement button is clicked:
class MutableNumberView {
constructor(model, controller) {
this.model = model;
this.controller = controller;
}
registerIncrementCallback(onIncrementCallback) {
this.onIncrementCallback = onIncrementCallback;
}
registerDecrementCallback(onDecrementCallback) {
this.onDecrementCallback = onDecrementCallback;
}
renderUpdate() {
this.numberSpan.textContent = this.model.value;
}
renderInitial() {
this.container = document.createElement('div');
this.numberSpan = document.createElement('span');
this.incrementButton = document.createElement('button');
this.decrementButton = document.createElement('button');
this.incrementButton.textContent = '+';
this.decrementButton.textContent = '-';
this.incrementButton.onclick =
() => this.onIncrementCallback();
this.decrementButton.onclick =
() => this.onDecrementCallback();
this.container.appendChild(this.numberSpan);
this.container.appendChild(this.incrementButton);
this.container.appendChild(this.decrementButton);
this.renderUpdate();
return this.container;
}
}
This View has two rendering methods: renderInitial will carry out the initial render, which sets up the DOM elements, and then the renderUpdate method is responsible for updating the number whenever it changes.
Tying this all together, our simple program would be initialized like so:
const model = new MutableNumberModel(5);
const view = new MutableNumberView(model);
const controller = new MutableNumberController(model, view);
document.body.appendChild(view.renderInitial());
view is given access to model so that it can retrieve the data to render. controller is given to both model and view so that it can glue them together by setting up the appropriate callbacks.
In the case of a user clicking the + (increment) button, the following process would kick off:
You may be wondering why we bother with the separation between the Controller and the Model. Why can't the View just communicate with the Model directly and vice versa? Well, it can! But if we did that, we'd be polluting both our View and our Model with more logic and hence more complexity. We could equally just place everything in the View and have no Model, but you can imagine how unwieldy that would get. Fundamentally, the degree and quantity of separation will vary with every project you pursue. At its core, MVC teaches us about the general idea of how to separate the problem domain from its presentation. How we wield this separation is up to us.
Since 1978, when MVC was first coined, many adaptations of it have surfaced, but its central theme of separation between Model and View has persisted through the decades. Consider the architectural design of a React application. It includes Components, which contain the logic for rendering state, and typically will include several domain-specific reducers, which take actions (for example, user has clicked something!) and derive state from those actions.
This architecture looks surprisingly similar to traditional MVC:

MVC, as a general guiding pattern, has impacted the design of countless frameworks and code bases throughout the last few decades, and it will continue to do so. Not every adaptation, reproduction, or MVC will abide by the original description posed in 1978 but, usually, these adaptations will stay true to the centrally important theme of separating a Model from its View and of having a View be a reflection (or even, a derivation) of a Model.
MVVM is similar in spirit to its ancestor, MVC. It prescribes a strict separation between the underlying business logic and data that drives a program and the rendering of that data:
The relationship between these parts is illustrated in the following diagram:

The MVVM architecture is more popular in frontend JavaScript as it suits the need of having a constantly updated View, while traditional MVC is more popular on the backend as it caters well to the simple render-once nature of most HTTP responses.
Within MVVM, the data binding between the ViewModel and the View usually uses DOM events to track user intent and then mutates data on the Model, which then emits mutation events of its own that can be listened for by the ViewModel, resulting in the View being constantly kept up to date with changing data.
Many frameworks will have their own adaptation of data-binding. Angular, for example, allows you to specify in your HTML templates a custom attribute called ng-model, which will tie a user input element such as <input> to a given data model, allowing data to flow in both directions. If the Model is updated, <input> will be updated to reflect that and vice versa.
Throughout your time as a JavaScript programmer, you will encounter variations of both MVC and MVVM. As patterns, they are infinitely applicable as they are concerned with the very basic tenets of a software system: the input of data into a system, the processing of that data, and the subsequent output of that processed data. There are a few other ways we could choose to architect these tenets into a code base, but it's likely that, in the end, almost every time, we'll end up with a system that delineates these concerns in a spirit similar to MVC (or MVVM).
Now that we have a firm idea of how we might architect a code base and the types of delineations that characterize a well-designed architecture, we can explore the individual pieces of that code base: the modules themselves.
In JavaScript, the word module has changed over the years. A module used to be any piece of code that was distinct and self-contained. A few years ago, you might have expressed several modules within the same file like so:
// main.js
// The Dropdown Module
var dropdown = /* ... definition ... */;
// The Data Fetcher Module
var dataFetcher = /* ... definition ...*/;
Nowadays, however, the word module tends to refer to Modules (capital M) as prescribed by the ECMAScript specification. These Modules are distinct files imported and exported across a code base via import and export statements. Using such Modules, we might have a DropdownComponent.js file that looks like this:
// DropdownComponent.js
class DropdownComponent {}
export default DropdownComponent;
As you can see, it uses the export statement to export its class. If we wish to use this class as a dependency, we would import it like so:
// app.js
import DropdownComponent from './DropdownComponent.js';
Both the import and export statements permit a variety of syntaxes. These allow you to define the names of what you're exporting or importing. In a scenario where a Module is only exporting one item, it is conventional to use export default [item] as we have done in DropdownComponent.js. This ensures that any dependents of the Module can import it and name it as they wish, as shown in this example:
import MyLocallyDifferentNameForDropdown from './DropdownComponent.js';
In contrast to this, you can specifically name your exports by declaring them within curly braces and using the as keyword:
export { DropdownComponent as TheDropdown };
This will mean that any importers will need to specifically specify the name of TheDropdown, like so:
import { TheDropdown } from './DropdownComponent.js';
Alternatively, you can export named items by having specific declarations inline with your export statements, such as var, const, let, function declarations, or class definitions:
// things.js
export let x = 1;
export const y = 2;
export var z = 3;
export function myFunction() {}
export class MyClass {}
On the importing side, such named exports can be imported by, again, using curly braces:
import { x, y, z, myFunction, MyClass } from './things.js';
When importing, you can also optionally designate the local name of that import with the as keyword to have its local name be different to its exported named (this is especially useful in cases of naming conflicts):
import { MyClass as TheClass } from './things.js';
TheClass; // => The class
MyClass; // ! ReferenceError
It's conventional to aggregate exports in areas of your code that provide several related abstractions. For example, if you have composed a small Component library, where each Component exports itself as default, then you could have index.js that exposes all of the Components together:
// components/index.js
export {default as DropdownComponent} from './DropdownComponent.js';
export {default as AccordianComponent} from './AccordianComponent.js';
export {default as NavigationComponent} from './NavigationComponent.js';
We can, very conveniently, now import our entire set of Components by using the asterisk with our import statement:
// app.js
import * from 'components/index.js';
// Make use of the imported components:
new DropdownComponent();
new AccordianComponent();
new NavigationComponent();
There are some additional nuances and complexities around modules in JavaScript, especially when considering the legacies of Node.js, that we, unfortunately, don't have time to go into, but what we've covered so far should give you a good enough coverage of the topic to be productive and nicely paves the way for us to explore the topic of modular design patterns.
Modular design patterns are the structures and syntactic conventions we use to craft individual modules. We would usually employ these patterns within distinct JavaScript Modules. Each distinct file should offer up and export a specific abstraction.
The Constructor pattern uses a singular constructor and then manually fills its prototype with methods and properties. This was the traditional approach for creating classical OOP-like classes in JavaScript before the class definition syntax existed.
Typically, it begins with the definition of a constructor as a function declaration:
function Book(title) {
// Initialization Logic
this.title = title;
}
This would then be followed by assigning individual methods to the prototype:
Book.prototype.getNumberOfPages = function() { /* ... */ };
Book.prototype.renderFrontCover: function() { /* ... */ };
Book.prototype.renderBackCover: function () { /* ... */ };
Or it would be followed by replacing the entire prototype with an object literal:
Book.prototype = {
getNumberOfPages: function() { /* ... */ },
renderFrontCover: function() { /* ... */ },
renderBackCover: function () { /* ... */ }
};
The latter approach tends to be preferred as it's more encapsulated and succinct. Nowadays, of course, if you wished to use the Constructor pattern, you would likely opt for method definitions as they take up less space than individual key-value pairs:
Book.prototype = {
getNumberOfPages() { /* ... */ },
renderFrontCover() { /* ... */ },
renderBackCover () { /* ... */ }
};
The instantiation of a constructor would be via the new keyword:
const myBook = new Book();
This creates a new object that has an internal [[Prototype]] of the constructor's prototype (that is, our object, which contains getNumberOfPages, renderFrontCover, and renderBackCover).
The Constructor pattern is useful in scenarios where you wish to have an abstraction that encapsulates the concept of a noun, that is, a thing that would make sense to have an instance of. Examples may include NavigationComponent or StorageDevice. The Constructor pattern allows you to create abstractions akin to traditional OOP classes. So, if you're coming from a classical OOP language, then you can feel free to use the Constructor pattern where you may have previously used classes.
If you're not sure whether the Constructor pattern is applicable, consider whether the following questions are true:
If the concept you're abstracting does not fulfill any of the preceding criteria, then you may want to consider another modular design pattern. An example of this may be a utility module that has various helper methods. Such a module may not require construction since it is essentially a collection of methods, and these methods and their behaviors would not vary between instances.
To achieve inheritance with the Constructor pattern, you need to manually make your prototype objects inherit from your parent constructor's prototype.
At the risk of over simplifying, we'll illustrate this with the classic example of an Animal super-class and a Monkey subclass. Here is our definition of Animal:
function Animal() {}
Animal.prototype = {
isAnimal: true,
grow() {}
};
Technically, to achieve inheritance, we want to create an object that has [[Prototype]] of Animal.prototype prototype and then use that newly-created object as our sub-class prototype subclass. The end goal is a prototypal tree that looks like this:
Object.prototype
└── Animal.prototype
└── Monkey.prototype
The easiest way to create an object with a given [[Prototype]] is with Object.create(ThePrototype). Here, we can use it to extend Animal.prototype and assign the result to Monkey.prototype:
function Monkey() {}
Monkey.prototype = Object.create(Animal.prototype);
We can then freely assign methods and properties to this new object:
Monkey.prototype.isMonkey = true;
Monkey.prototype.screech = function() {};
If we now try to instantiate Monkey, then we should be able to access not only its own methods and properties but also those we inherited from Animal.prototype:
new Monkey().isAnimal; // => true
new Monkey().isMonkey; // => true
typeof new Monkey().grow; // => "function"
typeof new Monkey().screech; // => "function"
It can be quite cumbersome to individually set a prototype's properties and methods one at a time, as shown in this example:
Monkey.prototype.method1 = ...;
Monkey.prototype.method2 = ...;
Monkey.prototype.method3 = ...;
Monkey.prototype.method4 = ...;
Due to this, another pattern has emerged to make things easier: using Object.assign(). This allows us to set properties and methods in bulk as object literals, and it means we can make use of the method definition syntax as well:
function Monkey() {}
Monkey.prototype = Object.assign(Object.create(Animal.prototype), {
isMonkey: true,
screech() {},
groom() {}
});
Object.assign here will assign any properties from its second (and third, fourth, and so on) arguments to the object passed as the first argument. This provides us with a more succinct syntax for adding properties to our child prototype object.
The Constructor pattern and its inheritance conventions have largely lost favor due to the newer class definition syntax, which allows a more succinct and simpler way to harness prototypal inheritance in JavaScript. As such, the very next thing we will explore is the Class pattern, which uses this newer syntax.
The Class pattern, which relies on the newer class definition syntax, has largely replaced the Constructor pattern. It involves the creation of classes, analogous to classical OOP languages, although behind the scenes it uses the same prototypal mechanism that the Constructor pattern uses. So, it can be said that it's just a bit of extra syntactic sugar to make the language a bit more expressive.
Here is an example of a basic class that abstracts the concept of a name:
class Name {
constructor(forename, surname) {
this.forename = forename;
this.surname = surname;
}
sayHello() {
return `My name is ${this.forename} ${this.surname}`;
}
}
The creation of a class via this syntax is effectively the creation of a constructor with an attached prototype, hence the following code is exactly equivalent:
function Name(forename, surname) {
this.forename = forename;
this.surname = surname;
}
Name.prototype.sayHello = function() {
return `My name is ${this.forename} ${this.surname}`;
};
Using the Class pattern is certainly aesthetically preferable to the clunky and older Constructor pattern, but do not be misled! Behind the scenes, exactly the same mechanisms are at play.
The Class pattern, much like the Constructor pattern, is useful when you have a self-contained concept that fulfills the following criteria:
Here are some examples of concepts that abide by these criteria and are hence reasonable to express via the Class pattern:
Typically such cases will stick out quite obviously to you. If you're having trouble, consider the use cases of your abstraction and try to write some consumer code, that is, pseudo code that utilizes your abstraction. If it seems sensible and doesn't feel too awkward to use, then you've probably landed on a good pattern.
Static methods and properties can be declared by using the static keyword:
class Accounts {
static allAccounts = [];
static tallyAllAccounts() {
// ...
}
}
Accounts.tallyAllAccounts();
Accounts.allAccounts; // => []
These properties and methods could also easily be added after the initial class definition:
Accounts.countAccounts = () => {
return Accounts.allAccounts.length;
};
Static methods are useful when you have a method or property whose functionality and existence are semantically related to the entire class as opposed to a singular instance.
To declare a public field (that is, a property) on your instance, you can simply declare this within the class definition syntax in line:
class Rectangle {
width = 100;
height = 100;
}
These fields are initialized for each instance and are, therefore, mutable on the instance itself. They're most useful when you need to define some sensible default for a given property. This can then be easily overridden within the constructor:
class Rectangle {
width = 100;
height = 100;
constructor(width, height) {
if (width && !isNaN(width)) {
this.width = width;
}
if (height && !isNaN(height)) {
this.height = height;
}
}
}
You can also define private fields by prefixing their identifier with a # symbol:
class Rectangle {
#width = 100;
#height = 100;
constructor(width, height) {
if (width && !isNaN(width)) {
this.#width = width;
}
if (height && !isNaN(height)) {
this.#height = height;
}
}
}
Private fields are only accessible by the class itself. Sub-classes do not have access:
class Super { #private = 123; }
class Sub { getPrivate() { return this.#private; } }
// !SyntaxError: Undefined private field #private:
// must be declared in an enclosing class
Private fields should be used with extreme caution as they can severely limit the extensibility of your code, hence increasing its rigidity and lack of flexibility. If you use a private field, you should ensure that you have considered the consequences. It may be the case that what you need is, in fact, just a pseudo-private field, prefixed with an underscore (for example, _private) or another obscure piece of punctuation (for example, $_private). Doing this will, by convention, ensure that fellow programmers making use of your interface will (hopefully) understand that they should not make use of the field publicly. If they do so, then the implication is that they may break things. If they wish to extend your class with their own implementation, then they can make use of your private field freely.
Inheritance within the Class pattern can very simply be achieved by using the class ... extends syntax like so:
class Animal {}
class Tiger extends Animal {}
This will ensure that any instance of Tiger will have [[Prototype]], which itself has [[Prototype]] of Animal.prototype:
Object.getPrototypeOf(new Tiger()) === Tiger.prototype;
Object.getPrototypeOf(Tiger.prototype) === Animal.prototype;
Here, we have confirmed that each new instance of Tiger has [[Prototype]] of Tiger.prototype and that Tiger.prototype inherits from Animal.prototype.
Conventionally, an extension is used not only to create semantic sub-classes but also to provide mixins of methods. JavaScript provides no native mixing-in mechanism so to achieve it you, either need to augment the prototype after the definition or effectively inherit from your mixins (as if they are superclasses).
Augmenting a prototype with your mixins is the simplest approach. We can achieve this by specifying mixins as objects and then adding them to prototype of a class via a convenient method such as Object.assign:
const fooMixin = { foo() {} };
const bazMixin = { baz() {} };
class MyClass {}
Object.assign(MyClass.prototype, fooMixin, bazMixin);
This approach, however, does not allow MyClass to override its own mixin methods:
// Specify MyClass with its own foo() method:
class MyClass { foo() {} }
// Apply Mixins:
Object.assign(MyClass.prototype, fooMixin, bazMixin);
// Observe that the mixins have overwritten MyClass's foo():
new MyClass().foo === fooMixin.foo; // true (not ideal)
This is expected behavior but creates a headache for us in some cases. Therefore, to achieve a more generalized mixin approach, we can explore a different mechanism. Instead of directly mixing-in methods to an existing prototype object, we can use inheritance. This can most easily be achieved by so-called Subclass Factories. These are essentially just functions that themselves return a class that extends a specified super-class:
const fooSubclassFactory = SuperClass => {
return class extends SuperClass {
fooMethod1() {}
fooMethod2() {}
};
};
Here's an example of how it might work in reality:
const greetingsMixin = Super => class extends Super {
hello() { return 'hello'; }
hi() { return 'hi'; }
heya() { return 'heya'; }
};
class Human {}
class Programmer extends greetingsMixin(Human) {}
new Programmer().hi(); // => "hi"
We can additionally implement a helper to combine any number of these Subclass Factories. It can do so by constructing a chain (or tree) of [[Prototype]] links that is as long as the list of mixins we provide:
function mixin(...mixins) {
return mixins.reduce((base, mixin) => {
return mixin(base);
}, Object);
}
And here's how we would make use of our mixin helper: first, we'd define our sub-class factories (that is, the actual mixins):
const alpha = Super => class extends Super { alphaMethod() {} };
const bravo = Super => class extends Super { braveMethod() {} };
Then, we can construct a class definition using both of these mixins via the mixin helper:
class MyClass extends mixin(alpha, bravo) {
myMethod() {}
};
This means that the result's MyClass instances will have access to its own prototype (containing myMethod), alpha's prototype (containing alphaMethod), and bravo's prototype (containing bravoMethod):
typeof new MyClass().myMethod; // => "function"
typeof new MyClass().alphaMethod; // => "function"
typeof new MyClass().braveMethod; // => "function"
Mixins can be awkward to get right, so it helps to utilize a library or proven piece of code to take care of this for you. The mixin mechanism you should use will probably depend on the exact characteristics you're seeking. In this section, we've seen two examples: one where we compose methods into a singular [[Prototype]] via Object.assign(), and another where we create a tree of inheritance (that is, a chain of [[Prototypes]]) to represent our mixin hierarchy. Hopefully, you are now in a better position to select which one of these (or indeed, all of the others available online) are best suited to your needs.
All functions within a class defined using the method definition syntax have the super binding available, which provides access to the super-class and its properties. The super() function is available to invoke directly (which will call the constructor of the super-class) and can provide access to specific methods (super.methodName()).
If you are extending another class and you are defining your own constructor, you must call super() and you must do so before any other code within your constructor that modifies the instance (that is, this) in any way:
class Tiger extends Animal {
constructor() {
super(); // I.e. Call Animal's constructor
}
}
If your constructor attempts to call super() after modifying the instance, or if it attempts to avoid calling super(), then you will receive ReferenceError:
class Tiger extends Animal {
constructor() {
this.someProperty = 123;
super();
}
}
new Tiger();
// ! ReferenceError: You must call the super constructor in a derived class
// before accessing 'this' or returning from the derived constructor
The super binding and its oddities are described in greater detail in Chapter 6, Primitives and Built-in Types (see the section on Function bindings).
The Prototype pattern involves using plain objects to act as templates for other objects. The Prototype pattern extends this template object directly without fussing with instantiation via new or Constructor.prototype objects. You can think of it as similar to conventional constructor or Class patterns minus the constructor.
Typically, you'll first create an object to act as your template. This will have all of the methods and properties associated with your abstraction. In the case of an inputComponent abstraction, it may look like this:
const inputComponent = {
name: 'Input Component',
render() {
return document.createElement('input');
}
};
Using our inputComponent template, we can then create (or instantiate) specific instances using Object.create:
const inputA = Object.create(inputComponent);
const inputB = Object.create(inputComponent);
As we've learned, Object.create(thePrototype) simply creates a new object and sets its internal [[Prototype]] property to thePrototype, meaning that any properties accessed on the new object will be looked for on thePrototype if they are not available on the object itself. As such, we can treat the resulting object just like any other classical instance, accessing properties as we would on instances resulting from the more conventional Constructor or Class patterns:
inputA.render();
For convenience, we could also introduce a method on inputComponent itself designed to do the object creation work:
inputComponent.extend = function() {
return Object.create(this);
};
This means that we can create individual instances with slightly less code:
const inputA = inputComponent.extend();
const inputB = inputComponent.extend();
And if we wish to create other types of inputs, then we can easily extend inputComponent, as we already have; add some methods to our the resulting object; and then offer that new object up for others to extend:
const numericalInputComponent = Object.assign(inputComponent.extend(), {
render() {
const input = InputComponent.render.call(this);
input.type = 'number';
return input;
}
});
To override a particular method and access its parent, as you can see, we need to reference and call it directly (InputComponent.render.call()). You may expect that we should be able to use super.render() but, unfortunately, super only refers to [[Prototype]] of the object (the home) on which the containing method is defined. And because Object.assign() effectively steals these methods off their home objects, super would refer to the wrong thing.
The Prototype pattern is most useful in scenarios where you have an abstraction that will have varying characteristics between instances (or extensions) but does not require construction. At its core, the Prototype pattern really only refers to the extension mechanism (that is, via Object.create), so it can equally be used in any scenario where you have objects that may semantically relate to other objects via inheritance.
Imagine a scenario in which we need to represent sandwich data. Every sandwich has a name, a bread type, and three slots for ingredients. For example, here is the representation of a BLT:
const theBLT = {
name: 'The BLT',
breadType: 'Granary',
slotA: 'Bacon',
slotB: 'Lettuce',
slotC: 'Tomato'
};
We may wish to create an adaptation of the BLT, reusing most of its characteristics except the Tomato ingredient, which will be replaced with Avocado. We could simply clone the object wholesale, by using Object.assign to copy all the properties from theBLT to a fresh object and then specifically copying over (that is, overwriting) slotC:
const theBLA = Object.assign({}, theBLT, {
slotC: 'Avocado'
});
But what if the BLT's breadType was changed? Let's take a look:
theBLT.breadType = 'Sourdough';
theBLA.breadType; // => 'Granary'
Now, theBLA is out of sync with theBLT. We have realized that what we actually want here is an inheritance Model so that breadType of theBLA will always match breadType of its parent sandwich. To achieve this, we can simply change our creation of theBLA so that it inherits from theBLT (using the Prototype pattern):
const theBLA = Object.assign(Object.create(theBLT), {
slotC: 'Avocado'
});
If we later change a characteristic of theBLT, it will helpfully be reflected in theBLA via inheritance:
theBLT.breadType = 'Sourdough';
theBLA.breadType; // => 'Sourdough'
This constructor-less Model of inheritance, as you can see, can be useful in some scenarios. We could equally represent this data using straightforward classes but with such basic data that may be overkill. The Prototype pattern is useful in that it provides a simple and explicit mechanism of inheritance that can result in less clunky code (although, equally, if misapplied, can lead to more complexity).
The Revealing Module pattern is a pattern used to encapsulate some private logic and then expose a public API. There are a few adaptations of this pattern, but usually it is expressed via an Immediately Invoked Function Expression (IIFE) that returns an object literal containing the public methods and properties:
const myModule = (() => {
const privateFoo = 1;
const privateBaz = 2;
// (Private Initialization Logic goes here)
return {
publicFoo() {},
publicBaz() {}
};
})();
Any functions returned by the IIFE will form a closure around their respective scopes, meaning that they will continue to have access to the private scope.
An example of a real-world Revealing Module would be this simple DOM Component that contains logic for rendering a notification to users:
const notification = (() => {
const d = document;
const container = d.body.appendChild(d.createElement('div'));
const message = container.appendChild(d.createElement('p'));
const dismissBtn = container.appendChild(d.createElement('button'));
container.className = 'notification';
dismissBtn.textContent = 'Dismiss!';
dismissBtn.onclick = () => {
container.style.display = 'none';
};
return {
display(msg) {
message.textContent = msg;
container.style.display = 'block';
}
};
})();
The notification variable in the outer scope will refer to the object returned by the IIFE, meaning we have access to its public API:
notification.display('Hello user! Something happened!');
The Revealing Module pattern is especially useful in scenarios where you need to have a delineation between private and public, where you have specific initialization logic, and where, for whatever reason, your abstraction does not suit more object-oriented patterns (Class or Constructor patterns).
Before the existence of class definitions and #private fields, the Revealing Module pattern was the only easy way to emulate real privacy. As such, it has somewhat fallen out of favor. Some programmers still make use of it but, usually, only due to aesthetic preferences.
The Conventional Module pattern is usually expressed as a plain object literal with a set of methods:
const timeDiffUtility = {
minutesBetween(dateA, dateB) {},
hoursBetween(dateA, dataB) {},
daysBetween(dateA, dateB) {}
};
It's quite typical for such a module to also reveal specific initialization methods such as initialize, init, or setup. Alternatively, we may want to provide methods that change the state or configuration of the entire module (such as setConfig):
const timeDiffUtility = {
setConfig(config) {
this.config = config;
},
minutesBetween(dateA, dateB) {},
hoursBetween(dateA, dataB) {},
daysBetween(dateA, dateB) {}
};
The Conventional Module pattern is incredibly flexible since it is just a plain object. JavaScript's treatment of functions as first-class citizens (that is, they're just like any other value) means that you can easily compose the objects of methods from functions defined elsewhere, as well:
const log = () => console.log(this);
const library = {
books: [],
addBook() {},
log // add log method
};
Conventionally, you may have considered using an inheritance or mixin pattern to include this log method in our library module, but here we're simply composing it ourselves by referencing and inserting it directly into our object. This pattern gives us a lot of flexibility in terms of how we reuse code in JavaScript.
The Conventional Module pattern is useful in any scenario where you simply wish to wrap up a set of related methods or properties into something with a common name. They are often used for collections of common methods that somehow relate to each other, such as logging utilities:
const logger = {
log(message) { /* ... */ },
warn(message) { /* ... */ },
error(message) { /* ... */ }
};
The Conventional Module pattern is just an object, so it's arguably unnecessary to even mention it at all. But, technically, it is an alternative to other techniques of abstraction definition, so it's useful to designate it as a pattern of its own.
The Class pattern is quite quickly become the de facto pattern for creating abstractions of all types, including singletons and utility objects as well, so it may not always be the case that your class will need to be utilized as a conventionally OOP class with inheritance and instantiation. For example, we may wish to set up a utility object with a class definition so that we can define any initialization logic within the constructor and provide a semblance of encapsulation within its methods:
const utils = new class {
constructor() {
this.#privateThing = 123;
// Other initialization logic here...
}
utilityA() {}
utilityB() {}
utilityC() {}
};
utils.utilityA();
Here, we're creating and immediately instantiating a class. This is similar in spirit to the Revealing Module pattern where we utilize an IIFE to encapsulate initialization logic and the public API. Here, instead of achieving that encapsulation via a scope (and the resulting closure around private variables), we are using the straightforward constructor to define our initialization. We then are using the regular instance properties and methods to define both our private variables and our public interface.
Singletons are useful when only one instance of a class is required. The singular instance produced is similar in nature to the Conventional or Revealing Module pattern. It enables you to wrap up an abstraction with the option of private variables and implicit construction logic. Common use cases of singletons include Utilities, Logging, Caching, Global Event Buses, and so on.
Deciding on which architectural and modular design patterns to use can be a tricky process, as usually at the time of deciding, it may not be immediately obvious what all of the requirements of the project are. Also, we as programmers are not omniscient. We are flawed, egoistic, and usually passionate individuals. This combination, if not kept in check, can yield chaotic code bases with designs that block the very productivity, reliability, and maintainability that we are trying to foster. To be wary of these pitfalls, remember the following:
In this chapter, we have explored the purpose and application of design patterns in JavaScript. This has spanned a foundational introspection on what it even means for something to be a design pattern and an exploration of some common modular and architectural design patterns. We have explored the various ways we can declare abstractions using JavaScript's native mechanisms such as classes and prototypes, and some more novel mechanisms such as the Revealing Module pattern. Our deep coverage of these patterns will ensure that, in the future, we have ample options available to us when crafting our abstractions.
In the next chapter, we will be exploring real-world challenges encountered by JavaScript programmers, such as state management and network communication, and applying our knowledge of new perspectives to them.
Many challenges a JavaScript programmer faces may not be with the language itself but with the ecosystems that their code must exist within and interface with. JavaScript is usually used in the context of the web, on either a browser or server, and so the problem domains encountered are usually characterized by topics such as HTTP and the DOM. We often have to wrestle with frameworks, APIs, and mechanisms that can sometimes seem awkward, unintuitive, and complicated. In this chapter, we're going to familiarize ourselves with some of the most common challenges and the approaches and abstractions we can use to surmount them.
We will begin by exploring the DOM and the challenges inherent in building ambitious Single-Page Applications (SPAs) in JavaScript. We'll then explore the topics of dependency management and security as these are both increasingly vital competencies in today's landscape. This chapter is not intended as an exhaustive coverage of topics, but rather a quick whirlwind of deep-dives that you may find relevant to the weighty task of crafting clean JavaScript on today's web platform.
In this chapter, we will cover the following topics:
The Document Object Model (DOM) API is provided within browsers to allow developers to read from and dynamically mutate web documents. Upon its initial introduction in 1997, it was very limited in scope but has expanded greatly in the last two decades, allowing us to now have programmatic access to a wide variety of browser functionality.
The DOM itself presents us with a hierarchy of elements that are derived from the parsed HTML of a given page. This hierarchy is made accessible to JavaScript via an API. This API allows us to select elements, traverse trees of elements, and inspect element properties and characteristics. Here is an example of a DOM tree with the corresponding JavaScript used to access it:

The way we access specific DOM nodes has changed over the years but its fundamental tree-like structure has remained the same. Via access to this structure, we can read from the elements, mutate them, or indeed add to the tree of elements ourselves.
Alongside the DOM API is a collection of other natively provided browser APIs that make it possible to do things such as reading cookies, mutating local storage, setting up background tasks (workers), and operating on the CSS Object Model (CSSOM).
As recently as the year 2012, it was quite typical for web developers to only use JavaScript to enhance experiences already manifested in the markup of a page. For example, they might've simply added a rollover state to a button or validation to a form field. Such additions were considered a type of progressive enhancement, where the user could experience the website without JavaScript if they wanted but having JavaScript enabled would enhance their experience in some small way.
Nowadays, however, it is far more common to have the frontend portion of a web application built almost entirely with JavaScript and expressed within a single page. These are often termed SPAs. Instead of having the user naturally navigate around a website, loading up new pages within the browser upon each action, the SPA will instead rewrite the current page's content and the current browser address. SPAs are therefore dependent upon the user's browser supporting JavaScript and potentially other APIs too. SPAs typically do not gracefully degrade, although it is best practice (and good sense!) to provide a series of fallbacks so that the entire audience can receive functionality.
The proliferation of the SPA can be attributed to both developer experience and user experience boosts:
The growing demands on web applications and the proliferation of the SPA have meant that programmers have come to rely much more on browser APIs, especially the DOM, to create rich and dynamic experiences. The painful truth, however, is that the DOM was never intended to cater to the creation of rich desktop-like experiences. Because of this, there have been many growing pains in bringing the DOM up to scratch with current demands. Additionally, there has been a slow and iterative process of creating frameworks that enable the development of rich experiences atop a platform that was not originally designed for them.
One of the most obvious ways in which the DOM (and browser APIs generally) does not meet the current demands of SPAs is experienced when trying to bind the DOM to data. We will now explore this topic in more depth.
One specific challenge that multiple frameworks have attempted to solve over the years is the binding of the DOM to data. We briefly discussed data-binding in the last chapter's section on MVVM. Any GUI needs to have a way of having its displayed pixels reflect its underlying data.
Via the DOM, we can dynamically create specific elements and place them as we wish. The user can then impose their intent on the application by interfacing with these elements, usually via input fields and buttons. These user actions, which we bind to via DOM events, may then affect a change in underlying data. This change needs to be reflected in the DOM. This back-and-forth is usually termed two-way-binding. Historically, to achieve this, we would manually create a DOM tree, set up event listeners on elements, and then manually mutate those DOM elements when any underlying data (or state) changed.
The challenge in manually fiddling with the DOM ourselves is that it doesn't scale very well without some kind of abstraction. It is easy enough to take a piece of data and derive a DOM tree from that data, but having the DOM tree tied to changes within the data and having the data tied to user-derived changes in the DOM (for example, clicking on buttons) are quite burdensome things to implement.
To illustrate this challenge, consider a simple shopping list in the form of an array composed of individual items as strings:
const shoppingList = ['Bananas', 'Apples', 'Chocolate'];
Deriving a DOM tree from this data is quite simple:
const ul = document.createElement('ul');
shoppingList.forEach(item => {
const li = ul.appendChild(document.createElement('li'));
li.textContent = item;
});
document.body.appendChild(ul);
This code would produce the following DOM tree (and append it to <body>):
<ul>
<li>Bananas</li>
<li>Apples</li>
<li>Chocolate</li>
</ul>
But what happens if our data changes? And what would happen if there were <input> via which users could add new items? To accommodate these things, we would have to implement an abstraction to hold our data and raise events (or invoke callbacks) whenever the data changes. Additionally, we'd need some way of tying each individual data item to a DOM node. If the first item, "Bananas" were to be changed to "Melons", then we should only make the minimum mutations necessary to the DOM to reflect that change. In this case, we would want to replace the first <li> element's inner text node's data property (in other words, the actual text contained within the text node):
shoppingList[0] = 'Melons';
ul.children[0].firstChild.data = shoppingList[0];
This type of change, in abstract terms, is known as DOM reconciliation and involves reflecting any changes made to data within the DOM. There are broadly three types of reconciliation:
DOM reconciliation is a relatively simple process. We could easily create ShoppingListComponent ourselves with the ability to update/add/remove items, but it would be very highly coupled to the structure of the data and the DOM. The logic pertaining just to a singular update may involve, as we've seen, the specific mutation of a text node's content. If we want to change our DOM tree slightly or the structure of our data, then we have to significantly refactor our ShoppingListComponent.
Many modern libraries and frameworks have sought to make this a less burdensome process by abstracting the DOM reconciliation process away behind a declarative interface. A good example of this is React, which allows you to declare your DOM tree declaratively using its JSX syntax within your JavaScript. JSX looks like regular HTML with the addition of interpolation delimiters ({...}) where regular JavaScript can be written to express data.
Here, we are creating a component that produces a simple <h1> greeting populated with an uppercase name:
function LoudGreeting({ name }) {
return <h1>HELLO { name.toUpperCase() } </h1>;
}
The LoudGreeting component could be rendered to <body> like so:
ReactDOM.render(
<LoudGreeting name="Samantha" />,
document.body
);
And that would result in the following:
<body>
<h1>HELLO SAMANTHA</h1>
</body>
We might implement a ShoppingList component in the following way:
function ShoppingList({items}) {
return (
<ul>
{
items.map((item, index) => {
return <li key={index}>{item}</li>
})
}
</ul>
);
}
And then we could render it in the following way, passing our specific shopping list items in our invocation of the component:
ReactDOM.render(
<ShoppingList items={["Bananas", "Apples", "Chocolate"]} />,
document.body
);
This is a simple example but gives us an idea of how React works. The true magic of React is in its ability to selectively re-render the DOM in reaction to changes in data. We can explore this in our example by changing data in reaction to a user action.
React and most other frameworks give us a straightforward mechanism of event-listening so that we can listen for user events in the same manner as we would conventionally. Via React's JSX, we can do the following:
<button
onClick={() => {
console.log('I am clicked!')
}}
>Click me!</button>
In our case of the shopping list problem domain, we want to create <input />, which can receive new items from users. To accomplish this, we can create a separate component called ShoppingListAdder:
function ShoppingListAdder({ onAdd }) {
const inputRef = React.useRef();
return (
<form onSubmit={e => {
e.preventDefault();
onAdd(inputRef.current.value);
inputRef.current.value = '';
}}>
<input ref={inputRef} />
<button>Add</button>
</form>
);
}
Here, we are using a React Hook (called useRef) to give us a persistent reference that we can re-use between component renders to reference our <input />.
To our ShoppingListAdder component, we are passing an onAdd callback, which we can see is called whenever the user has added a new item (in other words, when the <form> submits). To make use of a new component, we want to place it within ShoppingList and then respond when onAdd is invoked by adding a new item to our list of food:
function ShoppingList({items: initialItems}) {
const [items, setItems] = React.useState(initialItems);
return (
<div>
<ShoppingListAdder
onAdd={newItem => setItems(items.concat(newItem))}
/>
<ul>
{items.map((item, index) => {
return <li key={index}>{item}</li>
})}
</ul>
</div>
);
}
As you can see, we are using another type of React Hook called useState to persist the storage of our items. initialItems can be passed into our component (as an argument) but we then derive a set of persistent items from these that we can mutate freely across re-renders of our component. And that's what our onAdd callback is doing: it is adding a new item (entered by the user) to the current list of items:

Calling setItems will, behind the scenes, invoke a re-render of our component, causing <li>Coffee</li> to be appended to the live DOM. Creations, updates, and deletions are all handled similarly. The beauty of abstractions like React is that you don't need to think of these mutations as distinct pieces of DOM logic. All we need to do is derive a component/DOM tree from our set of data and React will figure out the precise changes needed to reconcile the DOM.
To ensure we understand what's going on, when a piece of data (state) is changed via a Hook (for example, setItems(...)), React does the following:
Other modern frameworks borrow from this approach as well. One nice side-effect of DOM reconciliation mechanisms built into these abstractions is that, via their declarative syntax, we can derive a deterministic tree of components from any given data. This is in stark contrast to the imperative approach, within which we must manually select and mutate specific DOM nodes ourselves. The declarative approach gives us a functional purity that enables us to produce outputs that are deterministic and idempotent.
Building large web applications, even with the reconciliation puzzle out of the way, is still a challenge. Every component or view within a given page needs to be populated with its correct data and needs to propagate changes. We'll be exploring this challenge next.
When building a web application, you'll quickly run into the challenge of getting different parts or components within your page to talk to each other. At any single time, your application should be representing the exact same set of data. If something changes, either via user action or some other mechanism, that change needs to be reflected in all of the appropriate places.
This problem occurs at different scales. You may have a chat application where an entered message needs to be propagated to all participants as fast as possible. Or you may have a piece of data that needs to be represented several times within the same application view and hence all of these representations need to be kept in sync. For example, if a user changes their forename in a profile settings pane, then this should reasonably update other places in the visible application where their forename appears:

In a conventional non-SPA, the Save personal info button would simply submit a <form> and the page would then fully reload and a brand new chunk of markup with the updated state would be sent down from the server. Within an SPA, it is slightly more complicated. We would need to both submit the data to the server and then somehow update only the relevant portions of the page with the new data.
To solve this problem, we have to think carefully about the flow of data or state within our application. The challenge is reflecting the source of truth for the relevant data as soon as possible in all of the places it needs to be represented. To achieve this, we need a way for different parts of our code base to talk to each other. There are a couple of paradigms we can use here:
If we consider a user changing their forename via <input />, we can envisage the following distinct paths of data flow to all components that depend upon the forename data:

Fundamentally, these approaches achieve the same thing: they render data to the DOM. The crucial thing that differs is how the change, in this case, a mutation of the forename, is communicated throughout the application and where the data resides at any one time:
The state-oriented paradigm is the increasingly more prevalent approach as it enables us to think in a clearer way about our data and its representations. We can say that we have a single representation of the data and that we derive components (or UI) from that data. It's a functionally pure approach since a component is really just a deterministic mapping of data to a given UI. Since any given component only cares about its input data, it doesn't need to make too many assumptions about the context it lives within. For example, we may have a UserInfo component, with an expected input of four values:
{
forename: 'Leah',
surname: 'Brown',
hobby: 'Kites',
location: 'Edinburgh'
}
Since this component does not rely on any global events or other contextual assumptions, it can be easily isolated. This aids not only in comprehension and maintainability but also enables us to write simpler tests. The UserInfo component can be extracted and tested by itself, with no inter-dependencies with the application in which it will eventually reside.
React is a popular framework for expressing this state-oriented paradigm, but many other frameworks are following suit. In React, combined with JSX, we may express our UserInfo component like so:
function UserInfo({ forename, surname, hobby, location }) {
return (
<div>
<h1>User Info</h1>
<dl>
<dt>Forename:</dt> <dd>{forename}</dd>
<dt>Surname:</dt> <dd>{surname}</dd>
<dt>Hobby:</dt> <dd>{hobby}</dd>
<dt>Location:</dt> <dd>{location}</dd>
</dl>
</div>
);
}
Here, we can see that this component's output is merely a mapping of its input. Such a simple case of I/O can easily be tested and reasoned about. The beauty of this harks back to the Law of Demeter (LoD), which we covered in Chapter 4, SOLID and Other Principles, which tells us that the UserInfo component has no business knowing where its data comes from or where it is used; it only needs to fulfill its singular responsibility: from its four inputs, it simply needs to provide us with a DOM hierarchy—clean and beautiful.
There is, naturally, a lot more complexity in real-life web applications that we have not been able to draw out with our forename example. However, if we remember the basics of separating concerns, and building views or components that are well isolated and functionally pure, then there are few challenges we won't be able to solve cleanly.
When building web applications, we will likely need to mutate the address the user sees within the browser to reflect the current resource being accessed. This is a core tenet of how browsers and HTTP work. An HTTP address should represent a resource. And so, when the user wishes to change the resource they are viewing, the address should correspondingly change.
Historically, the only way of mutating the current URL within the browser would be for the user to navigate to a different page via an <a href> or similar. When SPAs started to become popular, however, JavaScript programmers needed to get creative. In the early days, a popular hack would be to mutate the hash component of a URL (example.org/path/#hash), which would give the user the experience of traversing a traditional website where each navigation or action would result in a new address and a new entry in the browser's history, hence enabling the use of the back-and-forward buttons in the browser.
A few years later, thankfully, the History API found its way into browsers and is now the standard for mutating the address in response to navigations or actions within an SPA. Specifically, this API allows us to manipulate the browser session history by pushing new states or replacing current ones. For example, when a user expresses a wish to change to the About Us view within a fictional SPA, we can express this as a new state pushed to their history, like so:
window.history.pushState({ view: 'About' }, 'About Us', '/about');
This would immediately change the address in the browser to '/about'. Typically, the calling code would also instigate the rendering of the associated view. Routing is the name given to these combined processes of rendering the new DOM and mutating the browser's history. Specifically, a router takes responsibility for the following:
To illustrate these responsibilities, we can create a simple router for an application that very simply displays Hello {color}! atop a background of that very color for any color represented in the path of the URL. Hence, /red will render a red background with the text, Hello red!. And /magenta will render a magenta background with the text, Hello magenta!:

And here is our implementation of colorRouter:
const colorRouter = new class {
constructor() {
this.bindToUserNavigation();
if (!window.history.state) {
const color = window.location.pathname.replace(/^\//, '');
window.history.replaceState({ color }, color, '/' + color);
}
this.render(window.history.state.color);
}
bindToUserNavigation() {
window.addEventListener('popstate', event => {
this.render(event.state.color);
});
}
go(color) {
window.history.pushState({ color }, color, '/' + color);
this.render(color);
}
render(color) {
document.title = color + '!';
document.body.innerHTML = '';
document.body.appendChild(
document.createElement('h1')
).textContent = 'Hello ${color}!';
document.body.style.backgroundColor = color;
}
};
With this router, we can then call colorRouter.go() with our color and it'll change the address and be rendered as expected:
colorRouter.go('red');
// Navigates to `/red` and renders "Hello red!"
There is, even in this simple scenario, some complexity in our router. When the user originally lands on the page via conventional browsing, for example, perhaps by typing example.org/red into the address bar, the state of the history object will be empty, as we have not yet informed that browser session that /red is tied to the piece of state, { color: "red" }.
To populate this initial state, we need to grab the current location.pathname (/red) and then extract the color from it by removing the initial forward-slash. You can see this logic in the colorRouter constructor function:
if (!window.history.state) {
const color = window.location.pathname.replace(/^\//, '');
window.history.replaceState({ color }, color, '/' + color);
}
For more complex paths, this logic can start to get quite complex. In a typical router, many different patterns of paths will need to be accommodated for. As such, usually, a URL parsing library will be used to properly extract each part of the URL and allow the router to route that address correctly.
Various routing libraries and routing abstractions within larger frameworks have emerged over the years. They are all slightly different in the interface they present to the programmer. React Router, for example, allows you to declare your independent routes as a series of React components via JSX syntax:
function MyApplication() {
return (
<Router>
<Route exact path="/" component={Home} />
<Route path="/about/:employee" component={AboutEmployee} />
</Router>
);
}
Vue.js, a different framework, provides a unique routing abstraction of its own:
const router = new VueRouter({
routes: [
{ path: '/', component: Home }
{ path: '/about/:employee', component: AboutEmployee }
]
})
You may notice that, in both examples, there is a URL path specified as /about/:employee. The colon followed by a given token or word is a common way to designate that a specific portion of the path is dynamic. It's typical to need to dynamically respond to a URL that contains a piece of identifying information concerning a specific resource. It's reasonable that all of the following pages should produce different content:
It would be incredibly burdensome to specify these all as individual routes (and near impossible with large datasets), so routers will always have some way of expressing these dynamic portions. The hierarchical nature of URLs is usually also mirrored in the declarative APIs provided by routers and will typically allow us to specify a hierarchy of components or views to render in response to such hierarchical URLs. Here's an example of a routes designation that could be passed to the Router service of Angular (another popular framework!):
const routes = [
{ path: "", redirectTo: "home", pathMatch: "full" },
{ path: "home", component: HomeComponent },
{
path: "about/:employee",
component: AboutEmployeeComponent,
children: [
{ path: "hobbies", component: EmployeeHobbyListComponent },
{ path: "hobbies/:hobby", component: EmployeeHobbyComponent }
]
}
];
Here, we can see that AboutEmployeeComponent is attached to the path of about/:employee and has sub-components that are each attached to the sub-paths of hobbies and hobbies/:hobby. An address such as /about/john/hobbies/kayaking would intuitively render AboutEmployeeComponent and within that would render EmployeeHobbyComponent.
You can probably observe here how intertwined a router is with rendering. It is indeed possible to have standalone routing libraries, but it's far more typical for frameworks to provide a routing abstraction themselves. This allows us to specify our routes alongside a view or component or widget, or whatever abstraction our framework provides for rendering things to the DOM. Fundamentally, although different on the surface, all of these frontend routing abstractions will achieve the same result.
Another real-world challenge that many JavaScript programmers will expose themselves to, whether they're predominately working on the client side or server side, is that of dependency management. We'll begin exploring this next.
Loading JavaScript within the context of a single web page used to be simple. We could simply place a couple of <script> tags somewhere within the document's source and call it a day.
Over the years, however, the complexity of our JavaScript has grown tremendously, alongside the demands of our users. Alongside this, our code bases have grown as well. It was, for a period, natural to just keep adding more and more <script> tags. At a certain point, though, this approach falters. Apart from the burden of multiple HTTP requests being made on every page load, this approach also made it hard for programmers to juggle their dependencies. JavaScript was typical, in those days, to spend time carefully ordering <script> placements so that, for any particular script, its dependencies were in place before it itself loaded.
It was not uncommon to see HTML markup like this:
<!-- Library Dependencies -->
<script src="/js/libs/jquery.js"></script>
<script src="/js/libs/modernizr.js"></script>
<!-- Util Dependencies -->
<script src="/js/utils/data.js"></script>
<script src="/js/utils/timer.js"></script>
<script src="/js/utils/logger.js"></script>
<!-- App Widget Dependencies -->
<script src="/js/app/widgets/Nav.js"></script>
<script src="/js/app/widgets/Tile.js"></script>
<script src="/js/app/widgets/PicTile.js"></script>
<script src="/js/app/widgets/PicTileImage.js"></script>
<script src="/js/app/widgets/SocialButtons.js"></script>
<!-- App Initialization -->
<script src="/js/app/init.js"></script>
This approach was expensive from a performance perspective as the browser had to fetch every resource before continuing to parse and render the remaining document. Large collections of inline scripts in the <head> of an HTML document were hence considered an anti-pattern as they would block the user from being able to use the website for a significant amount of time. Even moving scripts to the bottom of <body> wasn't ideal as browsers would still have to load and execute them serially.
Predictably, our increasingly complex applications started to outgrow this approach. Developers needed more performance and a finer level of control over script loading. Thankfully, over the years, various improvements have been made in how we manage dependencies, how we bundle them, and how we then serve our code bases to the browser.
In this section, we'll explore the improvements that have occurred over the years and will seek to understand what the current best practices are, as well.
Before 2010 (approximately), there were very few agreed upon methods of loading large and complex JavaScript code bases within the browser. Soon, however, developers created the Asynchronous Module Definition (AMD) format. This was the first popular attempt at a standard that prescribed the definition of modules within JavaScript. It included both the ability to declare dependencies on each module and an asynchronous loading mechanism. This was a vast improvement upon the slow and blocking nature of multiple inline <script> tags.
RequireJS was a popular library that supported this format. To use it, you only needed to place a single entry point <script> in your document:
<script data-main="scripts/main" src="scripts/require.js"></script>
The data-main attribute here would specify the entry point of our code base, which itself would then load the initial set of dependencies and initialize the application, like so:
requirejs([
"navigationComponent",
"chatComponent"
], function(navigationComponent, chatComponent) {
// Initialize:
navigationComponent.render();
chatComponent.render();
});
Each dependency would then define itself and its own dependencies, like so:
// The Navigation Component AMD Module
define(
// Name of the module
'navigationComponent',
// Dependencies of the module
['utilA', 'utilB'],
// Definition of the module returned from function
function (utilA, utilB) {
return /* Definition of navigationComponent */;
}
);
This is similar in spirit to modules as now specified in the ECMAScript specification, except AMD is not related to any particular language syntax. It was entirely a community-driven effort to bring something like modules to JavaScript.
The fact that AMD prescribed that each module was defined within a callback, to which dependencies could be passed, meant that loading utilities such as RequireJS could load all dependencies asynchronously and then invoke the callback when it was done. This was a significant boost to frontend JavaScript at the time because it meant we could quite easily load massive dependency graphs in a way that eased the process of writing the code (less dependency juggling) and enabled the code to be loaded into the browser in a non-blocking and more performant fashion.
At a similar time to AMD, a new standards-driven effort started to emerge called CommonJS. This sought to make the require(...) syntax a standard in various non-browser environments, with the hopeful, eventual intention of such syntax being supported on the frontend as well. Here's an example of a CommonJS module (this may appear familiar if you're accustomed to programming in Node.js):
const navigationComponent = require('components/navigation');
const chatComponent = require('components/chat');
module.exports = function() { /* exported functionality */ };
This became the standard in various non-browser environments such as Node.js, SproutCore, and CouchDB. It was also possible to compile your CommonJS modules into browser-consumable scripts similar to AMD using the CommonJS Compiler. Sometime after this, around 2017, ES Modules emerged. This gave us language support for import and export statements, effectively solving the historical challenge of how to define modules in JavaScript:
// ES Modules
import navigationComponent from 'components/navigation';
import chatComponent from 'components/chat';
export default function() { /* do stuff */ };
In Node.js, such modules must have filename suffixes of .mjs instead of .js so the engine knows to expect import and export and not the conventional CommonJS module definition syntax. In the browser, such modules can be loaded by using <script type="module">. However, even with ES Modules supported in browsers, it's still arguably preferable to build and bundle your JavaScript into conventional non-modular script tags. This is due to factors of performance and compatibility across browsers. Not to worry though: we can still use ES Modules when writing our code! Tools such as Babel can be used to compile and bundle the latest JavaScript syntax into JavaScript that is compatible across many environments. It's typical to set up a tool such as Babel as part of your build and development process.
In the past, there was no package manager available for the JavaScript community. Instead, individuals and organizations would typically release code themselves, enabling developers to manually download the latest releases. With the introduction of Node.js and npm, this all changed. Finally, there was a central repository of packages available to pull into our projects with ease. This wasn't only useful for server-side Node.js projects but also entirely frontend projects as well. The emergence of npm is likely one of the most significant events that precipitated the maturation of the JavaScript ecosystem.
Nowadays, every project that heavily involves JavaScript will set out its manifest in a top-level package.json file, usually specifying, at the very least, a name, a description, a version, and a list of versioned dependencies:
{
"name": "the-fruit-lister",
"description": "An application that lists types of fruits",
"version": "1.0",
"dependencies": {
"express": "4.17.1"
},
"main": "app/init.js"
}
There are a variety of available fields you can use in package.json so it's worth exploring the npm documentation to understand all of them. Here's a rundown of the most common ones:
Running npm install in the directory where package.json resides will cause npm to download the versions of dependencies that you have specified. When declaring dependencies, by default, npm will do so with a caret (^) attached, meaning that npm will pick the latest available version that complies with the major version specified. So, if you specify ^1.2.3, then anything up to 1.99.99 (and so on) may validly be installed.
There are several fuzzy version ranges that you can use:
Arguably, the greatest issue with npm is that the unchecked introduction of new packages and their granularity in terms of functionality has led to projects with incredibly large and unwieldy dependency graphs. It's not unheard of for there to be individual packages that only export a singular narrow utility function. For example, in addition to a generic string utility package, you may also find a specific string function as a package of its own, such as uppercase. These packages are not inherently problematic—many of them serve useful purposes—but having an unwieldy dependency graph can lead to problems of its own. Any popular package that either is compromised or has not followed SemVer religiously can result in a propagation of issues across the JavaScript ecosystem, eventually affecting your project.
It's recommended to use a dependency management system that ensures the integrity of downloaded packages with cryptographic hashes (a checksum that would highlight malicious changes), to ensure that the package you end up executing is definitely the one you intended to install and has not been compromised or damaged during transmission. Yarn is an example of such a system (see https://yarnpkg.com). It is effectively a more secure and efficient abstraction atop npm. In addition to being more secure, Yarn has the added benefit of avoiding inconsistent package resolution, which is when two installs of a given code base's dependencies will result in a potentially different set of downloaded dependencies (due to the potentially fuzzy nature of npm's version declarations). Such inconsistencies can result in the same code base behaving differently across two instances (a massive headache and harbinger of bugs!). Yarn stores the current locked dependency graph and corresponding versions and checksums in a yarn.lock file, which would look like this:
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
array-flatten@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
integrity sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
...
Here, we see just one dependency but there'd usually be hundreds if not thousands as it would have to include not only your direct dependencies but also dependencies of those dependencies.
Dependency management is a topic that has had much written about it, so if you look online, there is no shortage of opinions and solutions. Fundamentally, as our concern is clean code, we should go back to our principles. Foremost, what we should seek in both our dependency systems and the dependencies themselves is reliability, efficiency, maintainability, and usability. In the context of dependencies, when it comes to maintainability, we are interested in both our ability to maintain code that consumes and depends upon the dependency and the ability for the dependency's maintainers to keep the dependency up to date and bug-free.
In the land of JavaScript, around the same time as AMD and CommonJS started to emerge, the rise of command-line bundlers and build tools was also on the rise. This gave us the ability to bundle large dependency graphs into singular files that could be loaded with a single <script>. The proliferation of build tools such as GruntJS and gulp.js meant that, slowly, the JavaScript we wrote as programmers could be oriented to cleanliness and comprehension and not the loading idiosyncrasies of browsers. We could also begin to take advantage of spin-off languages and subsets such as CoffeeScript, TypeScript, and JSX. Such JavaScript adaptations could easily be compiled and then bundled into fully operable JavaScript sent down to the browser.
The world that we have now is one in which build and bundling tools are incredibly common. There are several specific build tools, such as Grunt, gulp.js, webpack, and Browserify. Additionally, developers can easily use the npm scripts directive to create shortcuts to common command-line instructions.
Generally, building involves any preparations that need to occur on development code bases to make them production-ready. This can include anything from linting your CSS to bundling your JavaScript. Bundling, specifically, is concerned with the compilation and collation of large dependency graphs (of JavaScript files) into single JavaScript files. This is necessary so that we can serve our JavaScript code bases to the browser in the most performant and compatible way. Bundling utilities will usually output a file with a hash of the file's content as part of the filename, for example, main-f522dccf1ff37b.js. This filename can then be dynamically or statically inserted into a <script> tag within your HTML to be served to the browser:
<script src="/main-f522dccf1ff37b.js"></script>
Having a hash of the file's contents in the filename ensures that browsers always load the updated file and do not rely on a previously cached version of it. These files are usually minified as well. Minification involves parsing your JavaScript and producing a functionally identical but much smaller representation of it where all possible measures have been taken to take up less space, such as shortening variable names and removing whitespace and newlines. This is used in combination with HTTP compression techniques (such as .gzip) to ensure that the transmission over HTTP from a server to a client is as small and quick as possible. Usually, you will have distinct development and production builds since some build steps, such as minification, would make development (and debugging!) harder.
Serving bundled JavaScript to the browser is usually done with a singular <script> tag referencing the bundled JavaScript filename, placed at somewhere within the HTML that you serve to the browser. There are several important performance considerations when selecting an approach. The most important metric is how quickly, from the time of the initial request, a user can start using the application. When loading up superWebApp.example.com, we can imagine the following possible latencies experienced by the user:
It's usually preferable to have the execution of your JavaScript occur last, when the browser has done everything else. However, this is not always ideal. Some JavaScript may be necessary to load vital resources, and so it should be executed as early as possible so those HTTP fetches can occur concurrently with the rest of the browser's preparatory work.
Placement of your primary bundled <script> (your main code base) is vital in determining when your JavaScript will be fetched, when it will execute, and what the state of the DOM will be when it executes.
Here's a rundown of the most popular <script> placements and their respective advantages:
Having <script defer> in <head> is usually preferable as it can be fetched as soon as possible, it won't block parsing, and it'll only be executed once parsing has completed. This tends to give the user the fastest experience if you're serving up one singular bundled script and gives your JavaScript a completely parsed DOM that it can manipulate and render within immediately.
Serving JavaScript to the browser is a simple thing, in truth. It is only complicated by the need for us to have our web applications perform quickly for the benefit of users. Increasingly complex JavaScript code bases produce increasingly large bundles, and so loading these bundles takes time. Hence, the loading performance of your JavaScript is something you'll likely need to take seriously and spend time investigating. Performance is something easily forgotten but incredibly important.
Another similarly easily forgotten topic in the JavaScript ecosystem is security, and that's what we'll now be exploring.
Security is a vital part of a reliable code base. From the user, there is an implicit assumption that any given piece of software will act according to its functional expectations and will not lead to the compromise of their data or devices. Clean code considers security as it does other functional expectations—as a vital requirement that should be carefully fulfilled and thoroughly tested.
Since JavaScript is predominantly used in a networked situation—either on the server side or client side, it is forever fraught with the possibility of security vulnerabilities. And the fact that browsers are, effectively, sandboxed vehicles of remote code execution means that our end users are susceptible to just as much risk as we are. To protect ourselves and our users, we need to have a diverse understanding of the types of vulnerabilities that exist and how to counteract them. There are reams and reams of intimidating information about security vulnerabilities in the wild. We cannot hope to cover all of them in this book, but hopefully, if we explore a couple of the common vulnerabilities, then we'll be more generally cautious and aware and can begin to understand the types of measures we should put in place.
Cross-Site Scripting (XSS) is a vulnerability that enables attackers to inject their own executable code (usually JavaScript) into the frontend of a web application so that browsers will execute as if it were trusted. There are many ways XSS can manifest but these can all be boiled down to two core types:
The way that either stored or reflected payloads are rendered within a page is crucial here. Traditionally, XSS vectors were limited to the server-side rendering of unescaped user-entered HTML. So, if Bob sets his social media account name to <script>alert("Bob's XSS")..., then when Bob's page is requested from the server, the markup returned will include that <script> ready to be parsed and executed by the browser. Nowadays, however, SPAs and websites that involve client-side rendering are far more common, meaning that instead of the server being at fault for allowing unescaped HTML into the document's markup, it is the client (the JavaScript code base) that is at fault for rendering dangerous content directly into the DOM. XSS attacks that rely on client-side rendering are hence often called DOM-based XSS.
We can imagine a scenario in which our JavaScript code base has a UserProfile component that renders the name and profile information of any user. Upon initialization, this component requests its data from a REST endpoint that looks like /profile/{id}.json, returning the following JSON:
{
"name": "<script>alert(\"XSS...\");</script>",
"hobby": "...",
"profielImage": "..."
}
This component then renders the received name to the DOM via innerHTML, without escaping or cleansing its contents:
class UserProfile extends Component {
// ...
render() {
this.containerElement.innerHTML = `<h1>${this.data.name}</h1>`;
this.containerElement.innerHTML += `<p>Other profile content...</p>`;
}
}
All users who render the UserProfile component are liable to execute arbitrary (potentially damaging) HTML. This would be an issue whether the arbitrary HTML comes from a reflected or a stored source.
The prevalence of common JavaScript frameworks that abstract DOM rendering means that an attacker need only find a vulnerability within a library or framework to attack thousands of different websites. Most frameworks, thankfully, by default, have interpolation mechanisms that force inserted data to be rendered as text, not HTML. React, for example, will always produce text nodes for any data inserted via JSX's interpolation delimiters (curly braces). We can see this in effect here:
function Stuff({ msg }) {
return <div>{msg}</div>
}
const msg = '<script>alert("Oh no!");</script>';
ReactDOM.render(<Stuff msg={msg} />, document.body);
This results in the data containing <script> to be literally rendered as text, so that the resulting innerHTML of the <body> element is this:
<div>
<script>alert("Oh no!");</script>
</div>
Because the potentially dangerous HTML was rendered as text, no execution can occur and the XSS attack is prevented. This isn't the only way that an XSS attack can occur though. It's common for client-side frameworks to have templating solutions that rely on inline <script> tags, like so:
<script type="text/x-template">
<!-- VueJS Example -->
<div class="tile" @click="check">
<div :class="{ tile: true, active: active }"></div>
<div class="title">{{ title }}</div>
</div>
</script>
This is a convenient way of declaring templates to be used in the later rendering of specific components, but such templates are often used in combination with server-side rendering and interpolation, and such a combination is liable to XSS if an attacker can force a dangerous string to be interpolated by the server into the template, like so:
<!-- ERB (Rails) Template -->
<script type="text/x-template">
<!-- VueJS Template -->
<h1>Welcome <%= user.data.name %></h1>
</script>
If user.data.name contains nefarious HTML, then there is nothing that our JavaScript can do on the client side to prevent the attack. By the time we render our code, it may even be too late.
In modern web applications, we have to be wary of XSS, either stored or reflected, rendered on both the server and the client. It's a mind-bending combination of possible vectors, so it's crucial to ensure that you're employing a combination of countermeasures:
These countermeasures are not exhaustive, so it's advisable to also have a thorough readthrough of the Open Web Application Security Project's (OWASP) Cross-Site Scripting Prevention Cheatsheet: https://cheatsheetseries.owasp.org/cheatsheets/Cross_Site_Scripting_Prevention_Cheat_Sheet.html.
As an added security measure, it's important to also configure an appropriate CSP.
CSP is a relatively new HTTP header that is available on all modern browsers. It is not universally supported or respected, so it should not be depended upon as our sole defense against XSS. Nonetheless, if correctly configured, it can prevent the majority of XSS vulnerabilities. Browsers that don't support CSP will fall back to their default behavior of the same-origin policy, which itself provides a level of crucial security.
The Content-Security-Policy header allows you to specify where different types of resources are allowed to be loaded from. It is essentially an origin whitelist that the browser will validate all outgoing requests against.
It can be specified as a regular HTTP header:
Content-Security-Policy: default-src 'self'
Or it can be specified as a meta tag:
<meta http-equiv="Content-Security-Policy" content="default-src 'self'">
The format of the value is one or more policy directives, separated by semicolons, where each policy directive starts with the fetch directive. These designate the type of resource (for example, img-src, media-src, and font-src), or the default (default-src) that all directives will fall back on if they're not separately specified. The fetch directive is followed by one or more space-separated sources, where each source specifies where resources can be loaded from for that resource type. Possible sources include URLs, protocols, 'self' (to refer to the document's own origin), and more.
Here are some examples of CSP values with explanations for each one:
What an appropriately restrictive CSP is will depend entirely upon your specific web application, what kind of user-generated content you may be dealing with, and the sensitivity of the data you deal with. Having an appropriate CSP not only protects you from creating potential vectors of XSS (by loading from potentially compromised origins) but can help to counteract executing XSS vulnerabilities as well. CSP defends against XSS in the following specific ways:
Subresource Integrity (SRI) is a security feature within browsers that allows us to verify that the resources they fetch are delivered without any unexpected manipulation or compromise. Such manipulation could potentially occur where the asset is served from (for example, your CDN is hacked) or during network transmission (for example, a middleman attack).
To verify your script, you must provide an integrity attribute that contains the name of a hashing algorithm (such as sha256, sha384, or sha512) and then the hash itself. Here's an example:
<script src="//cdn.example.com/foo.js" integrity="sha384-367drQif3oVsd8RI/DR8RsFbY1fJei9PN6tBnqnVMpUFw626Dlb86YfAPCk2O8ce"></script>
To generate that hash, you can use OpenSSL's CLI as follows:
cat FILENAME.js | openssl dgst -sha384 -binary | openssl base64 -A
In addition to using the integrity attribute on <script>, you can use it on <link> for the verification of CSS style sheets. To enforce SRI, you can use the helpful CSP header:
Content-Security-Policy: require-sri-for script; require-sri-for style;
Doing this will ensure that any scripts or style sheets that exist without an integrity hash will fail to load. Once fetched, if the provided integrity hash does not match the hash of the received file, then it will be ignored (as if it wasn't fetched). Using SRI together with CSP gives you a considerable defense against XSS.
Cross-Site Request Forgery (CSRF) is when commands, usually in the form of HTTP GET or POST requests, are transmitted from a user without their intent, by malicious code. A primitive example would be if a banking website at bank.example.com had an API endpoint that allowed logged-in users to transfer a given amount to a specified account number. The endpoint might be as follows:
POST bank.example.com/transfer?amount=5000&account=12345678
Even if users were authenticated via a session cookie on the bank.example.com domain, a malicious website could easily embed and submit <form> directing the transfer to their own account, like so:
<form
method="post"
action="//bank.example.com/transfer?amount=5000&account=12345678">
</form>
<script>
document.forms[0].submit();
</script>
Regardless of what HTTP method is used by the endpoint or what kind of request body or parameters it accepts, it is liable to a CSRF attack unless it ensures that the request comes from its own website. This problem is partially solved by the same-origin policy inherent to browsers, which prevents some types of requests from taking place (such as a JSON POST request via XHR or PUT/DELETE requests), but there is nothing inherent in the browser to prevent a user innocently clicking a link to a website or submitting a form that forges a malicious POST request. These actions are, after all, the entire purpose of the browser.
Since there is no inherent mechanism of the web that prevents CSRF, developers have come up with their own defenses. One common mechanism to prevent CSRF is with a CSRF token (which should really be called an Anti-CSRF Token). This is a generated key (random, long, and impossible to guess) that is sent down to the client with each regular request while also being stored on the server as part of the user's session data. The server will then require the browser to send that key along with any subsequent HTTP requests to verify the source of each request. So, instead of just two parameters, our /transfer endpoint will now have a third, the token:
POST bank.example.com/transfer?
amount=5000&
account=12345678&
token=d55lv90s88x9mk...
The server can then verify that the provided token exists on that user's session data. There are many libraries and frameworks that simplify this. There are also a variety of adaptations and configurations of this basic token mechanism. Some of them will only generate a token for a given amount of time, or a given request cycle, whereas others will provide a singular token for that user's entire session. There are also a variety of ways for the token to be sent downstream to the client. The most common is within the response payload as part of the document markup, usually in the form of a <meta> element in <head>:
<head>
<!-- ... -->
<meta name="anti-csrf-token" content="JWhpLxPSQSoTLDXm..." />
</head>
This can then be grabbed by JavaScript and sent with any subsequent GET or POST requests made dynamically by the JavaScript. Or in the case of a conventional website without client-side rendering, the CSRF token can be sent downstream directly embedded in the <form> markup as a hidden <input>, which naturally forms part of the form's eventual submission to the server:
<form>
<input
type="hidden"
name="anti-csrf-token"
value="JWhpLxPSQSoTLDXm..." />
<!-- Regular input fields here -->
<input type="submit" value="Submit" />
</form>
Whatever anti-CSRF measure you use, the crucial need is for every request that mutates a user's data or carries out a command to be verified as coming from a legitimate page within the web application itself and not some maliciously crafted external source. To get a more thorough understanding of CSRF and the available countermeasures, I recommend reading and fully digesting OWASP's CSRF Prevention Cheatsheet: https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html.
XSS and CSRF only touch the surface of the types of attacks that we should be prepared for. Defending against all possible vulnerabilities is incredibly challenging and usually unrealistic, but we'd be foolish to not write code that is resilient against the most prevalent ones. A good general understanding of the types of vulnerabilities that exist can help us to be generally cautious in the code we write.
XSS, as explored, is a very diverse vulnerability with many possible vectors of attack. But we can defend against it in a general way by consistently and correctly discerning between trusted and untrusted data. We can limit the possibility of untrusted data wreaking havoc by placing it in only very specific places, correctly escaping it, and ensuring that we have an appropriately restrictive CSP. Likewise, with CSRF, there are countless ways for an attacker to perform it, but having a solid Anti-CSRF Token mechanism will save you from most of them. All we can hope for in the realm of security, given our limited resources, is that we can have coverage against the majority of popular attacks.
Here's a rundown of some other popular vulnerabilities that are worth being aware of:
There is always the possibility of an attack, and so we need to build that risk into our system designs. We cannot expect to be immune to these vulnerabilities, but when they do occur, we can ensure that we can fix them quickly, communicate transparently with affected users, and ensure that we carefully consider how we can prevent such vulnerabilities from occurring again.
Whether we're creating a framework for developers or a UI for non-technical users, the consumers of our code will always expect it to behave securely. This expectation is increasingly being encoded into law (for example, in EU law, the General Data Protection Regulation (GDPR)), so it's crucial to take it seriously and spend a good amount of time on learning and prevention. The practice of security is yet another example of how clean code is not only about our syntax and design patterns but about the very significant ways that our code affects our users and their everyday lives.
In this chapter, we have explored a variety of real-world challenges—topics that any JavaScript programmer may find themselves exposed to both within the browser and on the server. Writing clean code in JavaScript is not only about the language itself but about the web ecosystem that it exists within and the demands that this brings. Through our explorations of the DOM, routing, dependency management, and security, we have hopefully gained an insight into the technicalities of the problem domains that JavaScript often deals with, and an appreciation for the many frameworks, libraries, and standards-driven APIs that exist to help us to deal with these problems.
In the next chapter, we'll delve into the art of writing clean tests, a vitally important task, not only because it gives us confidence in our own code, but because it ensures the kind of reliability that users rightfully expect from our software.
In this section, we'll learn about the various ways we can foster and defend a cleaner JavaScript code base with testing and tooling. Specifically, we'll learn how to write good tests that will protect us against regressions and unclean code. By doing this, we'll learn about various tools and automation processes that we can use to deliver higher-quality code in a team environment.
This section contains the following chapters:
At the beginning of this book, we set out the primary tenets of clean code. Among these was reliability. There truly is no greater way to confirm reliability than to expose your code base to continued and multivariate usage. This means having real users sit in front of your software and use it, for real. Only via this type of exposure can we understand whether our code truly fulfills its purpose. However, it is usually unreasonable, and possibly even dangerous, to conduct such real-life tests constantly. If code is changed, it is possible for a piece of functionality that a user relies on to falter or regress. To prevent such cases, and to generally confirm that our expectations are met, we write tests. Without a good suite of tests, we are passively and arrogantly closing our eyes and hoping that nothing goes wrong.
In this chapter, we'll be covering the following topics:
A software test is an automated procedure that makes assertions about a piece of code and then reports the success of those assertions back to you. A test may make assertions about anything from an individual function to the behavior of an entire feature.
Tests, much like the rest of our code, deal in layers of abstraction and granularity. If we were to test a car abstractly, we may simply seek to assert the following attributes:
Obviously, this is not a very useful set of assertions for car engineers, as these attributes are either incredibly obvious or insufficiently described. The assertion It drives is important, but without extra detail, all it expresses is a generic business-oriented objective. It's similar to a project manager asking for a software engineer to ensure that a user-login portal, for example, can allow users to log in successfully. It is the engineer's job to not only implement the user-login portal but to derive working tests that successfully investigate the truth of the assertion users can log in successfully. And it is not always easy to derive good tests from generic statements.
To correctly engineer a test, we must take the generic and abstract requirements and distill them to their granular and unabstracted details. In the case of us asserting that our car has a working horn, for example, we can distill it like so:
When we start to add crucial detail to our assertions, they become useful to us. We can use them as both guides of implementation and confirmations of functionality. Even with this added detail though, our statement is only an assertion or a requirement. Such requirements are a useful step in the design of software. In fact, we should be very reluctant to even begin implementing software until we have such levels of specificity.
If a client were to ask you to implement a payment form, for example, it would be wise to gather the exact requirements: what types of payments shall it accept? What other customer information requires collection? What regulations or constraints are we beholden to in our storage of this data? These expanded requirements then become the yardstick via which we, and the client, will measure completeness. It follows naturally that we can then implement these requirements as individual tests to confirm their existence in the software.
A good testing methodology will involve tests for all distinct parts of a code base and will provide the following benefits:
There are many second-order effects of a good testing methodology as well. The increased confidence in the code base by your colleagues will mean you can be more productive and make more significant changes more quickly, cutting costs and pain in the long run. The sharing of knowledge can enable both your colleagues and your users to perform their actions quicker, with more understanding and less overhead in time and expense. The ability to prove fulfillment enables teams and individuals to better communicate the value of their work to stakeholders, managers, and users.
Now that we've discussed the obvious benefits of tests, we can discuss how we should go about authoring them. At the core of every test is a set of assertions, so we'll now explore what we mean by assertion and how we can use assertions to encode our expectations.
There are many tools, terms, and paradigms of testing. The existence of so much complexity can seem intimidating but it's important to remember that, at the core, testing is really just about making assertions about how something works.
Assertions can be made programmatically by expressing either SUCCESS or FAILURE depending on a specific outcome, as in the following example:
if (sum(100, 200) !== 300) {
console.log('SUCCESS! :) sum() is not behaving correctly');
} else {
console.log('FAILURE! :( sum() is behaving correctly');
}
Here, we will receive a our FAILURE! log if our sum function is not giving the expected output. We can abstract this pattern of success and failure by implementing an assert function, like so:
function assert(assertion, description) {
if (assertion) {
console.log('SUCCESS! ', description);
} else {
console.log('FAILURE! ', description);
}
}
This can then be used to make a series of assertions with added descriptions:
assert(sum(1, 2) === 3, 'sum of 1 and 2 should be 3');
assert(sum(5, 60) === 65, 'sum of 60 and 5 should be 65');
assert(isNaN(sum(0, null)), 'sum of null and any number should be NaN');
This is the fundamental core of any testing framework or library. They all have a mechanism for making assertions and reporting both the success and failure of those assertions. It is also normal for testing libraries to provide a mechanism to wrap up or contain related assertions and, together, call them a test or test case. We can do something similar by providing a test function that allows you to pass a description and a function (to contain assertions):
function test(description, assertionsFn) {
console.log(`Test: ${description}`);
assertionsFn();
}
We can then use it like so:
test('sum() small numbers', () => {
assert(sum(1, 2) === 3, 'sum of 1 and 2 should be 3');
assert(sum(0, 0) === 0, 'sum of 0 and 0 should be 0');
assert(sum(1, 8) === 9, 'sum of 1 and 8 should be 9');
});
test('sum() large numbers', () => {
assert(
sum(1e6, 1e10) === 10001000000,
'sum of 1e6 and 1e10 should be 10001e6'
);
});
The produced testing log from running this would be as follows:
> Test: sum() small numbers
> SUCCESS! sum of 1 and 2 should be 3
> SUCCESS! sum of 0 and 0 should be 0
> SUCCESS! sum of 1 and 8 should be 9
> Test: sum() large numbers
> SUCCESS! sum of 1e6 and 1e10 should be 10001e6
From a technical perspective, the pure action of authoring assertions and simple tests is not too challenging. Writing a test for a singular function is rarely hard. However, to write entire test suites and to thoroughly test all parts of a code base, we must utilize several more complicated testing mechanisms and methodologies to help us out.
To recall the car analogy, let's imagine that we have a car sitting in front of us, and we wish to test its horn. The horn is not a standalone piece of machinery. It is embedded within the car and dependent on a power source separate to itself. In fact, what we may discover is that we must first start the car up via the ignition before the horn will work. And the success of an ignition is itself dependent upon several other components, including a working ignition switch, fuel in the tank, a working fuel filter, and a non-drained battery. The functionality of the horn is therefore dependent upon a series of many moving parts. So, our test of the horn becomes not only a test of the horn itself but effectively a test of almost the entire car! This is not ideal.
To get around this issue, we could hook the horn up to a separate power supply just for testing purposes. By doing this, we are isolating the horn, enabling the test to only reflect the functionality of the horn itself. In the testing world, this stand-in power supply we're using might be called a stub or a mock.
Our approach of isolating the power supply of the horn is unfortunately flawed. Even if our test is successful—and the horn works—we haven't guaranteed that the horn will still work when hooked up to the real power supply within the car. The isolated test of the horn is still, arguably, useful because it tells us about any failures within the horn's specific circuitry and mechanism, but it is not sufficient on its own. We need to test how the horn will work when it is embedded in the real-life situation of having to depend on other components. In software, we call such real-life tests integration tests or end-to-end tests, while the isolated tests are typically called unit tests. An effective testing methodology will always include both types:

There is a risk when isolating individuals parts for testing, of creating an unrealistic scenario in which you end up not actually testing the true functionality of a code base, but instead testing the efficacy of your mocks. Here, in our car analogy, isolating the horn by supplying it with a mock power supply enables us to purely test the horn's circuitry and sound-making mechanism and gives us a clear path to debugging issues if the test fails. But we need to complement this test with several integration tests so that we can be confident that the entire system works correctly. Even if we have a thousand unit tests for all parts of a system, there is no guarantee of a working system without testing the integration of all of these parts.
To ensure a thoroughly tested code base, we must engage in different types of testing. As touched on already, the unit test enables us to test isolated parts, while the various combinations of parts can be tested via either integration, functional, or E2E tests. It's useful first to understand what we mean when we talk about a part or a unit.
When we talk about a unit of code, there is admittedly a fuzziness to the concept. Typically, it will be a piece of code that has a singular responsibility within a system. When a user wishes to perform an action via our software, they will, in fact, be activating a series of parts of our code, all working together to give the user the output they desire. Consider an app in which users can create and share images. A typical user experience (a flow or journey) may involve a few distinct steps that all involve different parts of the code base. Every action the User performs, often without them knowing, will encapsulate a series of code actions:
Together, all of these steps, combined with all other steps a user could potentially take, can be considered a system. And a fully-tested system might involve unit tests for each individual step, integration tests for each pair of steps, and functional or End-to-End (E2E) tests for every combination of steps that together form a user flow or user journey. We can visualize the types of tests that may need to exist as part of a system as follows:

Here, we can see one Start point and two End points, indicating two distinct user journeys. Each dot can be thought of as a single area of responsibility or unit that is activated as part of these journeys. As you can see, a unit test is only concerned with a single area of responsibility. The integration test is concerned with two (or more) neighboring areas that integrate. And an E2E or functional test is concerned with all of the areas involved in a singular user journey. In the former example of our image-sharing app, we can imagine that we may have specific unit tests for actions such as uploading a photo to the CDN or sending push notifications, an integration test that tests the integration of the friends database, and an E2E test that tests the entire flow from creating to sharing a new image. Each of these testing methodologies would be vital in ensuring a truly well-tested system, and each has its own unique benefits as well as pitfalls and challenges to overcome.
As we described with our car analogy, a unit test is a test that deals with an isolated unit of code. This will usually be either a singular function or module that will make one or more simple assertions about the operation of the code.
Here are some examples of singular unit test scenarios:
There are several benefits of testing individually-isolated units of code:
There are, however, challenges that come unit-testing as well:
The unit test, as the most granular type of test, is vital to any code base. It is perhaps easiest to think of it as a type of double-entry bookkeeping system. When you make a change, you must reflect that change via an assertion. This implementation-then-testing cycle is best done in proximity—one after the other—perhaps via TDD, which will be discussed later. The unit test is your way of confirming to yourself that you truly wrote the code you intended to write. It provides a level of certainty and reliability that your team and stakeholders will be hugely grateful for.
Integration testing, as the name suggests, deals with integrations of distinct units of code. An integration test will provide a more useful signal about how your software will operate in production than simple unit tests. In our car analogy, an integration test might assert the functionality of the horn, based on how it operates with the car's own power supply, instead of providing a mock power supply. It may however still be a partially isolated test, ensuring it does not involve all components within the car.
Here are a couple of examples of possible integration tests:
There are quite a few advantages of integrating modules and testing their behavior together:
So,while unit tests give us a narrow and detailed view of the input and output of specific modules and functions, integration tests allow us to see how all of these modules work together and, by doing so, provide us with a view into potential problems of integration. This is incredibly useful, but there are traps and challenges to writing integration tests:
The integration test provides vital insight at the crucial points of interfacing and I/O that govern how all of the individual parts of a code base work together as a system. Integration tests often provide the most signal about potential faults in a system, as they are both usually quick to run and highly transparent upon failures (unlike potentially clunky E2E tests). Naturally, integration tests can only tell you things about the points of integrations they encapsulate. For more complete confidence in the functionality of a system, it's always a good idea to employ E2E testing.
E2E testing is a more extreme form of integration test where, instead of testing individual integrations between modules, we'll test the entire system, usually by executing a series of actions that would happen in reality to produce a given result. These tests are sometimes also called functional tests because they are interested in testing areas of functionality from the user's perspective. Well-constructed E2E tests give us confidence that our entire system is working correctly, but are most valuable when combined with more granular unit and integration tests so that faults can be more quickly and precisely identified.
Here's a quick lowdown of the benefits of writing E2E tests:
There are challenges involved in crafting E2E tests, however:
E2E testing, although challenging to get right, can provide a level of insight and confidence that is hard to get from only unit and integration tests. In terms of automated testing procedures, E2E testing is the closest we can reasonably get to getting our software in front of real users. It is the least granular and most systemic way of discerning whether our software works in the way our users expect it to, which, after all, is what we're most interested in.
TDD is a paradigm in which we write tests before implementation. In doing so, our tests end up informing and affecting the design of our implementation and its interface. By doing this, we begin to see tests as not only a form of documentation but a form of specification. Via our tests, we can designate how we wish something to work, writing assertions as if the functionality existed, and then we can iteratively build out the implementation such that all of our tests eventually pass.
To illustrate TDD, let's imagine that we wish to implement a word-counting function. Before implementing it, we can begin to write some assertions about how we wish for it to work:
assert(
wordCount('Lemonade and chocolate') === 3,
'"Lemonade and chocolate" contains 3 words'
);
assert(
wordCount('Never-ending long-term') === 2,
'Hyphenated words count as singular words'
);
assert(
wordCount('This,is...a(story)') === 4,
'Punctuation is treated as word boundaries'
);
This is a rather simple function and so we've been able to express most of its functionality in just three assertions. There are naturally other edge cases but we've pieced together enough expectations that we can begin to implement the function. Here is our first attempt:
function wordCount(string) {
return string.match(/[\w]+/g).length;
}
Immediately running this implementation via our small test suite, we receive the following results:
SUCCESS! "Lemonade and chocolate" contains 3 words
FAILURE! Hyphenated words count as singular words
SUCCESS! Punctuation is treated as word boundaries
The Hyphenated words test is failing. TDD, by its nature, expects iterative failure and refactor to bring an implementation inline with a test suite. Given this particular failure, we can simply add a hyphen to our regular expression's character class (between the [...] delimiters):
function wordCount(string) {
return string.match(/[\w-]+/g).length;
}
This produces the following test logs:
SUCCESS! "Lemonade and chocolate" contains 3 words
SUCCESS! Hyphenated words count as singular words
SUCCESS! Punctuation is treated as word boundaries
Success! Via incremental iteration, although simplified for the sake of illustration, we have implemented something via TDD.
As you may have observed, TDD is not a particular type or style of test, but rather it is a paradigm for when, how, and why we go about testing. The traditional view of testing as an afterthought is limited and often can force us into a position where we simply don't have time to write a good test suite. TDD, however, forces us to lead with a solid test suite, giving us a few notable benefits:
TDD is an especially useful paradigm when getting started with testing as it will force you to take a step back before implementing something and really consider what you're trying to do. This planning stage is really helpful in ensuring that our code fully aligns with user expectations.
In this chapter, we introduced the concept of testing and how it relates to software. While brief and introductory, these foundational concepts are crucial if we're going to approach testing with an aim toward reliability and maintainability. Testing, like many other concerns in the software world, can be liable to cargo culting, so it's crucial to retain a perspective on the fundamentals and the theory behind the tests we write. Testing, at its core, is about proving expectations and protecting against faults. We've covered the differences between unit, integration, and E2E tests, discussing both the advantages and challenges inherent in each.
In the next chapter, we'll look into how we can take this knowledge and apply it to crafting clean tests alongside real-life examples. Specifically, we will cover what measures and guiding principles we can use to ensure that our tests and the assertions within them are reliable, intuitive, and maximally useful.
In the last chapter, we covered the theory and principles underlying software testing. We delved into the benefits and challenges inherent in unit, integration, and E2E testing. In this chapter, we will take this knowledge and apply it to some real-world examples.
It's not sufficient to simply understand what testing is and see its merits from a business perspective. The tests we write form a significant part of our code bases, and should hence be crafted in the same careful manner as all other code we write. We want to craft tests that not only give us confidence that our code works as intended, but are themselves reliable, efficient, maintainable, and usable. We must also be wary of writing overly complex tests. Doing so can trap us in a situation where our tests increase the burden of understanding and lead to more overall complexity and flakiness in the code base, reducing overall productivity and satisfaction.
If wielded with care and caution, tests can give code bases a clarity and cleanliness that enables users and colleagues to carry out their work with greater speed and quality. In the following sections, we'll explore the best practices to stand by and potential pitfalls to avoid when writing tests.
In this chapter, we will cover the following topics:
One of the most important considerations when writing any test, whether a granular unit test or a far-reaching E2E test, is the question of what to test. It's entirely possible to test the wrong thing; doing so can give us false confidence in our code. We may write a huge test suite and walk away grinning, thinking that our code now fulfills all expectations and is utterly fault-tolerant. But our test suite may not test the things we think it does. Perhaps it only tests a few narrow use cases, leaving us exposed to many possibilities of breakage. Or perhaps it conducts tests in a way that is never emulated in reality, leading to a situation where our tests don't protect us from failures in production. To protect us against these possibilities, we must understand what we truly wish to test.
Consider a function that we've written to extract phone numbers of a specified format from arbitrary strings. The phone numbers can be in a variety of forms, but will always have between 9 and 12 digits:
Here is our current implementation:
function extractPhoneNumbers(string) {
return string.match(/(?:[0-9][- ]?)+/g);
}
We decide to write a test to assert the correctness of our code:
expect(
extractPhoneNumbers('my number is 0899192032')
).toEqual([
'0899192032'
]);
The assertions we use are vital. It's important that we are testing the right thing. With our example, this should include exemplar strings that contain a complete variety of input: strings that contain phone numbers, strings that contain no numbers, and strings that contain a mixture of phone numbers and non phone numbers. It's far too easy only to test the positive cases, but it is in fact equally important to check for the negative cases. In our scenario, the negative cases include situations where there are no phone numbers to be extracted and hence may consist of strings such as the following:
Very quickly, when composing such exemplar cases, we see this true scope of complexity that our implementation will have to cater to. Incidentally, this highlights the tremendous advantage of employing Test-Driven Development (TDD) to define expectations firmly. Now that we have a few cases of strings containing numbers that we do not wish to be extracted, we can express these as assertions, like this:
expect(
extractPhoneNumbers('123')
).toEqual([/* empty */]);
This currently fails. The extractPhoneNumbers('123') call incorrectly returns ["123"]. This is because our regular expression does not yet make any prescriptions about length. We can easily make this fix:
function extractPhoneNumbers(string) {
return string.match(/([0-9][- ]?){9,12}/g);
}
The added {9,12} part will ensure that the preceding group (([0-9][- ]?)) will only match between 9 and 12 times, meaning that our test of extractPhoneNumbers('123') will now correctly return [] (an empty array). If we repeat this testing-and-iteration process with each of our exemplar strings, we will eventually arrive at a correct implementation.
The key takeaway from this scenario is that we should seek to test the complete gamut of inputs that we may expect. Depending on what we're testing, we can usually say there's always a limited set of possible scenarios that any piece of code we write will cater to. We want to ensure that we have a set of tests that analyze this range of scenarios. This range of scenarios is often called the input space or input domain of a given function or module. We can consider something well-tested if we expose it to a representative variety of inputs from its input space, which, in this case, includes both strings with valid phone numbers and those without valid phone numbers:

It's not necessary to test every possibility. What's more important is to test a representative sample of them. To do this, it's essential first to identify our input space and then partition it into singular representative inputs that we can then individually test. For example, we need to test that the phone number "012 345 678" is correctly identified and extracted, but it would be pointless for us to exhaustively test the variations of that same format ("111 222 333", "098 876 543", and so on). Doing so would be unlikely to reveal any additional errors or bugs in our code. But we should definitely test other formats with different punctuation or whitespace (such as "111-222-333" or "111222333"). It's additionally important to establish inputs that may be outside of your expected input space, such as invalid types and unsupported values.
A full understanding of your software's requirements will enable you to produce a correct implementation that is well tested. So, before we even begin writing code, we should always ensure that we know exactly what it is we're tasked with creating. If we find ourselves unsure what the full input space might be, that's a strong indicator that we should take a step back, talk to stakeholders and users, and establish an exhaustive set of requirements. Once again, this is a strong benefit of test-led implementation (TDD), where these deficits in requirements are spotted early and can hence be resolved before costs are sunk into a pointless implementation.
When we have our requirements in mind and have a good understanding of the entire input space, it is then time to write our tests. The most atomic part of a test is its assertions, so we want to ensure we can effectively craft intuitive assertions that communicate our expectations well. This is what we'll be covering next.
The core of any test is its assertions. An assertion prescribes exactly what we expect to occur, and so it is vital not only that we craft it accurately but that we craft it in a way that our expectation is made utterly clear.
A single test will usually involve several assertions. And a test will typically follow the form of: given an input of X, do I receive an output of Y? Sometimes, establishing Y is complex and may not be constrained to a singular assertion. We may want to introspect Y to confirm that it is truly the desired output.
Consider a function named getActiveUsers(users), which will return only the active users from a set of all users. We may wish to make several assertions about its output:
const activeUsers = getActiveUsers([
{ name: 'Bob', active: false },
{ name: 'Sue', active: true },
{ name: 'Yin', active: true }
]);
assert(activeUsers.length === 2);
assert(activeUsers[0].name === 'Sue');
assert(activeUsers[1].name === 'Yin');
Here, we have clearly expressed our expectations for the output of getActiveUsers(...) as a series of assertions. Given a more fully-featured assertion library or more complex code, we could easily constrain this to a singular assertion, but it's arguably clearer to separate them.
Many testing libraries and utilities provide abstractions to aid us in making assertions. The popular testing libraries, Jasmine and Jest, for example, both provide a function called expect, which supplies an interface with many matchers, each individually allowing us to declare what characteristics a value should have, as in the following examples:
The exact implementation of these matchers may vary from library to library, and the abstraction and naming provided may also vary. Chai.js, for example, provides both the expect abstraction and a simplified assert abstraction, allowing you to assert things in the following fashion:
assert('foo' !== 'bar', 'foo is not bar');
assert(Array.isArray([]), 'empty arrays are arrays');
The most important thing when crafting an assertion is to be utterly clear. Just as with other code, it is unfortunately quite easy to write an assertion that is incomprehensible or hard to parse. Consider the following assertion:
chai.expect( someValue ).to.not.be.an('array').that.is.not.empty;
This statement, due to the abstractions provided by Chai.js, has the appearance of a human-readable and easily understandable assertion. But it is actually quite difficult to understand exactly what's going on. Let's consider which of the following this statement might be checking:
It is, in fact, checking that the item is both not an array and that it is non-empty—meaning that, if the item is an object, it'll check that it has at least one property of its own, and if it's a string, it'll check that its length is greater than zero. These true underlying mechanics of the assertion are obscured and so, when exposed to such things, programmers may be left in a state of either blissful ignorance (thinking the assertion works as they wish it to) or painful confusion (wondering how on earth it works).
It may be the case that what we wished to assert all along was simply whether someValue was both not an array but was array-like, and as such, had a length greater than zero. As such, we can lend clarity using Chai.js's lengthOf method in a new assertion:
chai.expect( someValue ).to.not.be.an('array');
chai.expect( someValue ).to.have.a.lengthOf.above(0);
To avoid any doubt and confusion, we could, alternatively, assert more directly without relying on Chai.js's sentence-like abstractions:
assert(!Array.isArray(someValue), "someValue is not an array");
assert(someValue.length > 0, "someValue has a length greater than zero");
This is arguably far clearer as it explains to the programmer the exact check that is taking place, eliminating the doubt that could arise with a more abstract assertion style.
The crux of a good assertion is its clarity. Many libraries provide fancy and abstract mechanics of assertion (via the expect() interface, for example). These can create more clarity, but if over used, they can end up being less clear. Sometimes, we just need to Keep it Simple, Stupid (KISS). Testing code is the worst possible place in which to get fancy with egotistic or mis-abstracted code. Simple and straightforward code wins every time.
Now that we've explored the challenge of crafting intuitive assertions, we can slightly zoom out and have a look at how we should craft and structure the tests that contain them. The next section reveals hierarchies as a helpful mechanism to communicate meaning through our test suites.
To test any code base, we would likely need to write a large number of assertions. Theoretically, we could have a long list of assertions and nothing else. However, doing this may make it quite difficult to read, write, and analyze the reports of tests. To prevent such confusion, it is common for testing libraries to provide some scaffolding abstractions around assertions. For example, BDD-flavoured libraries such as Jasmine and Jest supply two pieces of scaffolding: the it block and the describe block. These are just functions to which we pass a description and callback, but together, they enable a hierarchical tree of tests that makes it far easier to comprehend what's going on. Testing a sum function using this pattern might be done like so:
// A singular test or "spec":
describe('sum()', () => {
it('adds two numbers together', () => {
expect( sum(8, 9) ).toEqual( 17 );
});
});
Non-BDD libraries tend to surround groups of assertions with simpler infinitely-nestable test blocks, like so:
test('sum()', () => {
test('addition works correctly', () => {
assert(sum(8, 9) == 17, '8 + 9 is equal to 17');
});
});
As you can see, the naming of the BDD-flavored it and describe terms can help us to craft descriptions for our test suites that read like full English sentences (for example Describe an apple » It is round and sweet). This isn't enforced but gives us a useful nudge toward better descriptions. We can also infinitely nest describe blocks so that our descriptions can reflect the hierarchical nature of the thing we're testing. So, for example, if we were testing a math utility called myMathLib, we may imagine the following test suite with its various sub-suites and specifications:
This hierarchy naturally reflects the conceptual hierarchy of the abstraction we're testing. The reporting provided by the testing library will usefully reflect this hierarchy. Here's an example output from the Mocha testing library in which every test of myMathLib passes successfully:
myMathLib
add()
✓ can add two integers
✓ can add two fractions
✓ returns NaN for non-numeric inputs
subtract()
✓ can subtract two integers
✓ can subtract two fractions
✓ returns NaN for non-numeric inputs
PI
✓ is equal to PI at fifteen decimal places
Individual assertions come together to form tests. Individual tests come together to form test suites. Every test suite provides us with clarity and confidence regarding specific units, integrations, or flows (within E2E tests). The composition of these test suites is vital to ensuring that our tests are simple and comprehensible. We must take the time to think about how we will express the conceptual hierarchy of whatever we're testing. The test suites we create also need to be intuitively placed within the directory structure of our code base. This is what we'll explore next.
It can be said that the goal of testing is simply to describe what you have done. By describing, you are forced to assert your assumed truths about how something operates. When these assertions are executed, we can then discern whether our descriptions, our assumed truths, correctly reflect reality.
In the act of description, we must choose our words carefully so that they express our meaning clearly and comprehensibly. Tests are one of our last defenses against obscurity and complexity. Some code is unavoidably complicated, and we should ideally craft it in a way that reduces its obscure nature, but if we can't fully do this, then it is the role of tests to clear up any remaining confusion and provide the final point of clarity.
The key to clarity while testing is to focus purely on the perspective of the person who must read through the tests (or their logged outputs). Here are some specific points of clarity to remain aware of:
Consider the following test for AnalogClockComponent:
describe('AnalogClockComponent', () => {
it('works', () => {
const r = render(AnalogClockComponent, { time: "02:50:30" });
expect(rendered.querySelector('.mm-h').style.transform)
.toBe('rotate(210deg)');
expect(rendered.querySelector('.hh-h').style.transform)
.toBe('rotate(-30deg)');
expect(rendered.querySelector('.ss-h').style.transform)
.toBe('rotate(90deg)');
expect(/\btheme-default\b/).test(rendered.className)).toBe(true);
});
});
As you can see, this test makes several assertions about the transform CSS property of specific elements. It's possible to make an informed guess as to what these are, but the clarity could definitely be improved. To make this cleaner, we can use better names to reflect what we're testing, separate the tests to represent the different concepts being tested, use variable names to provide clarity about what values we're making assertions about, and use comments to explain any possibly unintuitive things:
describe('AnalogClockComponent', () => {
const analogClockDOM = render(AnalogClockComponent, {
time: "02:50:30"
});
const [
hourHandTransform,
minuteHandTransform,
secondHandTransform
] = [
analogClockDOM.querySelector('.hh-h').style.transform,
analogClockDOM.querySelector('.mm-h').style.transform,
analogClockDOM.querySelector('.ss-h').style.transform
];
describe('Hands', () => {
// Note: the nature of rotate/deg in CSS means that a
// time of 03:00:00 would render its hour-hand at 0deg.
describe('Hour', () => {
it('Renders at -30 deg reflecting 2/12 hours', () => {
expect(hourHandTransform).toBe('rotate(-30deg)');
});
});
describe('Minute', () => {
it('Renders at 210 deg reflecting 50/60 minutes', () => {
expect(minuteHandTransform).toBe('rotate(210deg)');
});
});
describe('Second', () => {
it('Renders at 90deg reflecting 30/60 seconds', () => {
expect(secondHandTransform).toBe('rotate(90deg)');
});
});
});
describe('Theme', () => {
it('Has the default theme set', () => {
expect(
/\btheme-default\b/).test(analogClockDOM.className)
).toBe(true);
});
});
});
You'll likely observe that the cleaner way is far longer, but when it comes to testing, it is truly best to bias yourselves toward such lengthy descriptiveness. Being over-descriptive is better than being under-descriptive because, in the latter case, your colleagues are left with a deficit of information, scratching their heads and making a possibly incorrect guess about functionality. When we provide a generous amount of clarity and explanation, we are helping a wider diversity of colleagues and users. If we are obscure and terse, however, we are specifically limiting the set of people who can understand our code, and hence limiting its maintainability and usability.
Now that we have explored the craft of exposing final clarity via a well-structured test suite, we can zoom out once more and discuss how we might communicate the purpose and types of tests we're writing via our directory structures and file naming conventions.
Our test suites should usually be constrained to individual files, to delineate areas of concern for our programmer-colleagues. Organizing these test files to form a coherent part of a larger code base can be a challenge, though.
Imagine a small JavaScript code base with the following directory structure:
app/
components/
ClockComponent.js
GalleryComponent.js
utilities/
timer.js
urlParser.js
It's quite typical to place tests relating to particular code in sub-directories close to where that code resides. In our example code base, we may create the following tests sub-directories to contain unit tests for our components and utilities:
app/
components/
ClockComponent.js
GalleryComponent.js
tests/
ClockComponent.test.js
GalleryComponent.test.js
utilities/
timer.js
urlParser.js
tests/
timer.test.js
urlParser.test.js
Here are some additional notes regarding conventions, which, as we should know by now, are vital in increasing the familiarity and intuitiveness of a code base and hence its overall cleanliness:
Once again, hierarchy is key here. We must ensure that the hierarchy of our directories helpfully mirrors the conceptual hierarchy of our code and its problem domain. As an equal and important part of a code base, a test should be placed carefully and appropriately within a code base, not as an afterthought.
In this chapter, we have applied our theoretical knowledge of testing to the practical craft of constructing real, working, and clean test suites. We looked at some of the pitfalls that exist in doing so, and we highlighted the important qualities to strive for, such as clarity, intuitive naming, and following conventions.
In the next chapter, we will be looking into a variety of tools we can use to help us to write cleaner code, from linters to compilers, and beyond!
The tools we use have a massive impact on the habits we fall into when writing code. When coding, just as in life, we want to gather good habits and avoid bad habits. An example of a good habit would be writing syntactically valid JavaScript. To help us enforce this good habit, we can use a linter to inform us when our code is invalid. We should consider each tool in this way. What good habit does it inspire? What bad habit does it discourage?
If we recall our original tenets of clean code (R.E.M.U) we can observe how various tools help us abide by them. Here's just a small collection of tools that would be of service to the four tenets:
Tools that inspire good habits work by augmenting our feedback loops. A feedback loop is whatever eventually makes you realize that you need to make a change. Perhaps you introduced a bug that caused an error to be logged. Perhaps your implementation is unclear and a colleague complained. If tools can catch these situations early, then it can speed up our feedback loop, enabling us to work faster and to a higher level of quality. In the following diagram, we illustrate Our Feedback Loop and how it is fed by information from tools at each stage of development:

Throughout our stages of development, there are many avenues of feedback. There are linters to tell us when our syntax is problematic, static type checkers to confirm we are using types correctly, and tests to confirm our expectations. Even after deployment, this feedback continues. We have error logs that indicate failure, analytics that tell us about user behavior, and feedback from end users and other individuals informing us about breakages or areas for improvement.
In this chapter, we'll be covering a small selection of the tools that can help us in building good habits and a positive feedback loop. Specifically, we're going to cover the following:
A linter is a tool used to analyze code and discover bugs, syntax errors, stylistic inconsistencies, and suspicious constructs. Popular linters for JavaScript include ESLint, JSLint, and JSHint.
Most linters allow us to specify what types of bugs or inconsistencies we would like to look for. ESLint, for example, will allow us to specify a global configuration for a given code base in a root-level .eslintrc (or .eslintrc.json) file. In it, we can specify the version of the language we are using, which features we are using, and which linting rules we would like to be enforced. Here's an example .eslintrc.json file:
{
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"extends": "eslint:recommended",
"rules": {
"semi": "error",
"quotes": "single"
}
}
Here's an explanation of our configuration:
We can try our configuration out by writing a piece of code that intentionally breaks the rules:
const message = "hello"
const another = `what`
if (true) {}
If we install and run ESLint on this code (within bash: > eslint example.js), then we'll receive the following:
/Users/me/code/example.js
1:7 error 'message' is assigned a value but never used
1:17 error Strings must use singlequote
1:24 error Missing semicolon
2:7 error 'another' is assigned a value but never used
2:17 error Strings must use singlequote
2:23 error Missing semicolon
4:5 error Unexpected constant condition
4:11 error Empty block statement
8 problems (8 errors, 0 warnings)
4 errors and 0 warnings potentially fixable with the `--fix` option.
This details all of the errors in the syntax according to our configured rules. As you can see, it details the rule that was broken and the line the problem was found on. ESLint and other linting tools can be incredibly helpful in finding hard-to-spot syntax errors, some of which may, if left untouched, lead to difficult to debug functional bugs in the future. Linting also gives the code more consistency, enabling programmers to feel a sense of familiarity and endure less cognitive burden, as would be the case in a code base with many different syntax conventions.
ESLint also includes a facility for fixing a subset of these syntax errors via its --fix option, although you may have noticed that only a subset of errors can be fixed this way. Others will need to be done manually. Thankfully, though, there are a number of more advanced tools available to help us out. Formatters, such as Prettier and Standard JS, will take our syntactic preferences and make active changes to our code to ensure that it remains consistent. This means that programmers don't have to burden themselves with specific syntactic rules, or endlessly change code in response to linters. They can write code in the manner they desire, and when they're done, the formatter will change the code to conform to the agreed upon syntax conventions or warn the programmer if there is a severe or invalid syntax error.
To illustrate, let's run Prettier with its default configuration on a simple piece of code:
function reverse( str ) {
return ( String( str ).split( '' ).reverse().join( '' ) );
}
When running the preceding code through Prettier, we receive the following:
function reverse(str) {
return String(str)
.split("")
.reverse()
.join("");
}
As we can see, Prettier has removed and changed some of our syntactic habits to its configured conventions. Namely, it has exchanged single quotes for double quotes, it has removed redundant parentheses, and it's made significant changes to the whitespace. The magic of formatters is that they take the pain away from the programmer. They do the work of correcting minor syntactic habits, leaving the programmer free to pursue more important work. The general trend in the industry is away from simple linters and toward more fully featured tools that combine both linting and formatting.
The decision over what syntactic conventions to abide by is configurable and entirely up to you. There are many strongly held opinions about this, but the most important tenet to uphold is consistency. I personally prefer single quotes to double quotes, for example, but if I'm working in a code base where double quotes are the established convention, then I'll have no qualms about changing my habits. Most of the time, syntactic preferences are just subjective and inherited norms, so what's important is not which norm we use, but whether or not we all abide by it.
Many of the norms we have grown used to within the JavaScript language have been guided by its dynamically typed nature. For example, we have become used to having to check manually for specific types in order to provide meaningful warnings or errors within our interfaces. For many, these norms have been challenging to adapt to, and they have grown desperate for a higher level of confidence in the types they use. Thus, people have brought various static typing tools and language extensions to JavaScript. We'll be exploring these next, and while we do, take note of how such static typing tools might change or improve your personal development feedback loop.
As we've explored at length, JavaScript is a dynamically typed language. If wielded carefully, this can be a great benefit, allowing you to work quickly and permit a level of flexibility in your code that enables colleagues to work with it less painfully. However, there are situations in which dynamic types can create the possibility of bugs and needless cognitive burdens for programmers. Statically typed compiled languages, such as Java or Scala, force the programmer to specify the types they are expecting at the point of declaration (or infer the type by how it is used, prior to execution).
Static typing has the following potential benefits:
Even though JavaScript is dynamically typed, there have been efforts to give JavaScript programmers the benefits of a static typing system. Two pertinent examples of this are Flow and TypeScript:
Both Flow and TypeScript allow you to declare the types that you are declaring, either alongside variable declarations or parameter declarations within functions. Here's an example of declaring a function that accepts productName (string) and rating (number):
function addRating(productName: string, rating: number) {
console.log(
`Adding rating for product ${productName} of ${rating}`
);
}
Both Flow and TypeScript generally allow the annotation of types following a declaration identifier in the IDENTIFIER: TYPE form, where TYPE can be any of number, string, boolean, and many more. They do differ in many ways though, so it's important to investigate both. Naturally, both Flow and TypeScript, and most other static type checking technologies for JavaScript, will require a build or compilation step in order to work, as they include syntax extensions.
Be aware that static typing is not an elixir. The cleanliness of our code is not only constrained to its ability to avoid type-related bugs and difficulties. We have to zoom out, in our perspective, and remember to consider the user and what they're trying to achieve via our software. It's quite common to see passionate programmers get lost in the minutiae of their syntax but forgo the bigger picture. So, to change tack slightly, we'll now explore E2E testing tools, as E2E testing can be as significant in its effect on the quality of a code base as the typing system or syntax we use, if not more!
In the last few chapters, we explored the benefits and types of testing, including an overview of E2E testing. The testing libraries we typically use to build test suites and make assertions rarely include E2E testing facilities, so it's necessary for us to find our own tooling for this.
The aim of an E2E test is to emulate user behavior upon our application and to make assertions about the application's state at various stages of user interaction. Typically, an E2E test will test a specific user flow, such as user can register new account or user can log in and buy product. Whether we're using JavaScript on the server side or the client side, if we're building a web application, it will be hugely beneficial to carry out such testing. To do so, we need to use a tool that can artificially create the user environment. In the case of a web application, the user environment is a browser. And thankfully, there are a large number of tools that can either emulate or run real (or headless) browsers that we can access and control via JavaScript.
An example of such a tool is Puppeteer, a Node.js library that provides an API to control Chrome (or Chromium). It can run either headless or non-headless. Here's an example in which we open a page and log its <title>:
import puppeteer from 'puppeteer';
(async () => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://example.com');
const titleElement = await page.$('title');
const title = await page.evaluate(el => el.textContent, titleElement);
console.log('Title of example.com is ', title);
await browser.close();
})();
Puppeteer provides a high-level API that allows the creation and navigation of browser pages. Within this context, using a page instance, we can then evaluate specific client-side JavaScript via the evaluate() method. Any code passed to this method will be run within the context of the document, and will, therefore, have access to the DOM and other browser APIs.
This is how we're able to retrieve the textContent property of the <title> element. You'll have noticed that much of Puppeteer's API is asynchronous, meaning that we have to either use Promise#then or await to wait for each instruction to complete. This may be bothersome, but considering the fact that the code is running and controlling an entire web browser, it makes sense that some tasks are asynchronous.
E2E testing is rarely embraced because it is perceived as being difficult. While that perception was accurate at one point, it is no longer so. With APIs like that of Puppeteer, we can easily launch our web application, trigger specific actions, and make assertions about the results. Here's an example of using Jest (a testing library) with Puppeteer to make an assertion about the text within the <title> element at https://google.com:
import puppeteer from 'puppeteer';
describe('Google.com', () => {
let page;
beforeAll(async () => {
const browser = await puppeteer.launch();
page = await browser.newPage();
await page.goto('https://google.com');
});
afterAll(async () => await browser.close());
it('has a <title> of "Google"', async () => {
const titleElement = await page.$('title');
const title = await page.evaluate(el => el.textContent, titleElement);
expect(title).toBe('Google');
});
});
Fetching a page, parsing its HTML, and producing a DOM that we can make assertions about is a very complex process. Browsers are incredibly effective at doing this, so it makes sense to utilize them in our testing process. After all, it is whatever the browser sees that will dictate what the end user sees. E2E tests give us realistic insights into potential breakages, and it's no longer hard to write or run them. They are immensely powerful for the clean coder especially, as they let us see the reliability of our code from a more user-oriented perspective.
As with many of the tools we've explored, E2E testing may be best integrated into our development experience via automation. We'll now explore this in brief.
As we have highlighted, there are a large number of tools available to help us write clean code. These tools can be activated manually, usually via a command-line interface (CLI) or sometimes within our IDEs. Usually, however, it is prudent to have them run as part of our various stages of development. If using source control, then this process will include a commitment or staging process and then a pushing or checking-in process. These events, when combined with the simple act of making changes to files, represent the three vital development stages that our tooling can use to generate their outputs:
It can greatly ease development to have your tooling activate automatically, however, this isn't a requirement. You can lint your code, run tests, transpile your CSS, or generate compressed assets all via the CLI, without having to fuss with automation. You may find this to be slower though, and it's more likely that your tooling will be used inconsistently amongst your team if it is not standardized into a set of automations. It may be the case, for example, that your colleague always runs tests before transpiling SCSS to CSS, while you tend to do it the other way round. This can result in inconsistent bugs and it works on my machine syndrome.
In this chapter, we have discovered the usefulness of tooling, highlighting its power to improve our feedback loops, and how it empowers us to write cleaner code. We have explored a number of specific libraries and utilities as well, giving us a flavor of what types of tools exist and the various ways in which our abilities and habits as programmers can be augmented. We've tried out linters, formatters, static type checkers, and E2E testing tools, and we've seen the merits of tooling at every stage of development.
The next chapter begins our journey into the art and science of collaboration; a vital ingredient for anyone who wants to write clean code. We'll begin with an exploration of how we can write clear and understandable documentation.
In this section, we'll cover the vital skills that are involved in collaborating and communicating with other people and how to navigate the need to refactor your code. In doing so, we'll discuss documentation, strategies of collaboration, and how to identify and advocate for change in your team, organization, or community.
This section contains the following chapters:
Documentation has a bad reputation. It is hard to find the motivation to write it, it's a nuisance to maintain, and our exposure to it over the years has convinced us that it is one of the driest and dullest methods of knowledge transfer. It doesn't have to be this way, though!
If we choose to focus entirely on the users, then our documentation can be simple and pleasant. To do this, we must first consider who the users of our documentation are. What do they want? Every user, whether a GUI end user or a fellow programmer, begins the journey of using our software with a task in mind. It's our duty, both within the software and through its documentation, to enable them to perform their task with as little pain and confusion as possible. With this in mind, in this chapter we'll explore what it might mean for us to construct such pain-free documentation. We'll specifically be covering the following:
The purpose of documentation is to communicate what a piece of software does and how to use it. We can split the characteristics of clean documentation into four aspects: a clean piece of documentation communicates the concept of the software, provides a specification of its behaviors, and contains instructions for how to perform specific actions. And it does all of this with a focus on usability. By the end of this section, we will hopefully understand the vital importance of the user in the craft of building clean documentation.
A clean piece of documentation will communicate the underlying concept of the software. It'll do this by explaining what the software's purpose is in a way that allows potential users to see how they might make use of it. This can be considered the educational part of documentation: setting out the terminology and paradigms that will allow the reader to easily comprehend the other parts of the documentation and the software it describes.
To properly express the concepts of a piece of software, it's necessary to step inside the shoes of your users, seeing things from their perspective and communicating with them on their terms:
Writing good documentation is a process of considering the user and then crafting the appropriate abstractions for them. You'll hopefully notice how incredibly similar this is to the process of crafting clean code. There are, in fact, very few differences. When building documentation, we are crafting a tool that the user can use to accomplish a specific set of tasks. It is our responsibility to craft it in such a way that users can easily accomplish their end goals without being overwhelmed by the sheer volume and complexity of the software:

Consider a project that has taken several weeks to complete. It is a JavaScript (JS) library called SuperCoolTypeAnimator that other programmers can use to create typeface transitions. It allows them to display to the user a block of text that is animated from one typeface to another (for example, from Helvetica to Times New Roman). It's a rather complex code base that calculates these transitions manually. The depth of its complexity has meant that you, the programmer, have discovered far more about ligatures, serifs, and path interpolation than you ever thought possible. After months of being immersed in this increasingly deep problem domain, it is understandably challenging for you to share the perspective of a user who has not had your level of exposure. Thus the first draft of your documentation might start in the following way:
SuperCoolTypeAnimator is an SVG glyph animation utility that allows the creation and frame-by-frame manipulation of transitions between source glyphs and their respective target glyphs, calculating appropriate transitional anchors on the fly.
Let's compare that to the following alternative intro:
SuperCoolTypeAnimator is a JS library that allows you to animate small pieces of text from one typeface to another typeface with ease.
As introductions, the latter is far more widely understandable and will allow even non-expert users to immediately understand what the library does. The former introduction, while informative, may result in current and potential users feeling confused or alienated. The entire purpose of the software we build is to abstract away complexity, wrapping it up in a neat and simplified way. Belaboring our users with complexity should be done with regret and consideration: it is usually the last resort.
Another factor highlighted by the two introductions is their usage of special terminology (such as glyph and anchor). The usage of such domain-specific terminology is a balancing act. If your users have a good understanding of the typeface/font problem domain, terms such as glyph and typeface may be appropriate. There is, arguably, a high likelihood that users interested in your library are also aware of such concepts. But the use of more nuanced terms such as transitional anchors may be a step too far. This is likely a term that you have used within your abstraction to describe a highly complex area of implementation. It is a useful term to you, and perhaps to anyone wishing to make changes to the library, but it is perhaps less useful to users of the library. Therefore, it would be wise to avoid it in our documentation's introduction.
As well as providing a concept for the software, good documentation will also provide a specification, detailing the specific characteristics and behaviors of the interfaces provided by your software. This part of the documentation details the contract that the user or programmer can expect to have when using the software.
The specification should ideally be the simplest part of the documentation to write, for the following reasons:
The overriding purpose of providing a specification is to answer specific questions that a user might have about the operation of your code:

The following is an example of a specification for a function called removeWords.
removeWords( subjectString, wordsToRemove );
This function will remove the specified words from the specified subject string, returning a new string to you. A word here is defined as a string of characters bound by word boundaries (\b). For example, specifying an "I like apple juice" subjectString and ["app", "juice"] for wordsToRemove would remove only "juice", as "app" exists in the subject but is not bound by a word boundary. The following are the arguments:
As you can hopefully tell, this specification is a purely technical explanation of a function's behavior. It tells the user exactly what arguments they must provide and what output they'll receive. When writing the specification portion of your documentation, the most important qualities to abide by are clarity and correctness. Be wary of the following traps:
The specification is arguably the most important part of your documentation, as it explains, in clear terms, the behavior of every part of your software's relevant APIs. Ensure that you take the same care and diligence when documenting your code as you would when writing it.
In addition to concept and specification, a clean piece of documentation will instruct a user in how to accomplish common tasks. These are commonly termed walkthroughs, tutorials, how-tos, or recipes.
Primarily, a user, regardless of whether they are a programmer or end user, is concerned with how to get from where they are to where they want to be. They are interested in knowing what steps to take. Without instructions for common use cases, they'll be left desperately piecing together what they know about your software from intuitions or other pieces of documentation. Consider a book about cookery that only details the ingredients and their behaviors when cooked, but doesn't contain any specific recipes that combine ingredients in a specific order. That'd be a challenging cooking book to make use of. While it may provide a highly detailed set of culinary information, it doesn't help users answer their actual questions:

When composing instructions, whether they're in the form or video tutorials or written walk-throughs, it is important to consider what use cases are most prevalent or challenging for your users. As with many things in life, you can only reasonably cater for the bulk of prospects, not all of them. It is unreasonable to create tutorials for every single possible use case. And likewise, it is unreasonable, from a user's perspective, for you to only provide a singular tutorial for the most common use case. It is wise to strike a compromise and have a small collection of tutorials that each express:
The instructional part of documentation is probably the most challenging. It requires us to take on the role of teacher and see things from another person's position of relative ignorance. Maintaining focus on the person we're teaching, the user, is absolutely vital. This feeds quite nicely into our final aspect of clean documentation: usability.
Usability is the final component in clean documentation. Just like our software, our documentation must be concerned with users and their specific needs. The previous three aspects (concept, specification, instruction) have focused on content, while usability is purely about the way in which we express that content. It's vitally important not to overwhelm or confuse user when they learn about your software:

There are many ways we can confuse and overwhelm. Among them are these the following:
In Chapter 2, The Tenets of Clean Code, we went into great detail on what usability means. We discussed how it was not only about intuitive design and accessibility, but also about the consideration of user stories—specific tasks that users wish to carry out and how to accommodate these. Documentation is no different to any other interface that we provide; it must solve the user's problems. Consider how you may design documentation to cater for these example user stories:
We may consider building different styles of documentation for the different types of information a user seeks. More specification-oriented information (for example, how does this specific framework function work?) may be best suited to a traditional long-form document format, while more instruction-oriented information (for example, how can I build an app with this framework?) may be best suited to rich media (for example, a video tutorial).
Due to the many types of information a user may seek, and all the different individual users we are catering for, it is absolutely worth dedicating significant time to planning, designing, and executing clean documentation. It should never be an afterthought.
Now that we're explored our four aspects of clean documentation, let's explore the incredible gamut of available mediums we can employ to express our documentation. We do not have to employ only a single dull, scrollable document: there are dozens of other ways we can inform and educate our users and colleagues.
If we generously define documentation as a way of learning about a piece of software, we can observe that there are dozens of different mediums of documentation that exist. Many of them are implicit or accidental; others are more intentionally crafted, either by the creator(s) of the software or the expert community that has gathered around it:
It's worth considering how all these mediums care catered for. When the official documentation does not help to solve a user's problem, what other pathways will they explore before abandoning your software altogether? How can we channel a user's difficulty or questions towards a solution as quickly and fluidly as possible? If a user is unlikely to read an entire specification document, then what other mediums can we create for them?
As we have seen, when writing documentation, there is a need to adapt the language used to the audience. To do this, we must have a good picture in our mind of who the audience is, what their current level of knowledge is, and what they're trying to accomplish. A notorious challenge for programmers is communicating with less technical or non-technical people. This is a very common and crucial part of their role as a creator of software. Whether communicating with end users at specific points in a UX, or collaborating with non-technical stakeholders, there is a need to tailor our communication to the audience. To do this, we should do the following:
Communicating with non-technical individuals may appear to be a distinct challenge, but it is no different from communicating with anyone else. As we should be doing all the time, we just need to meet the person where they're at and communicate according to their current understanding of the problem domain.
In this chapter, we have explored the difficult art of authoring clean documentation, breaking it down into the four vital aspects of clean documentation: concept, specification, instruction, and usability. We've discussed the challenge of correctly identifying our audience and how to craft our communications to suit them. This knowledge will not only be useful in crafting formal documentation, but also in our everyday communications with stakeholders and within our software when it needs to communicate with users.
In the next chapter, we move swiftly on to the unique challenge of dealing with other peoples' code. What happens when we, on the receiving end of potentially poor documentation or unintuitive code, need to be productive? We'll find out.
Humans, being complex and fickle, create complex and fickle things. However, dealing with other people and their code is an unavoidable part of being a programmer. Whether we deal with libraries and frameworks constructed by someone else or inherit entire legacy code bases, the challenges are similar. The first step should always be to seek an understanding of the code and its paradigms. When we have a full understanding of the code, we can begin to interface with it in a clean way, enabling us to create new functionality or make improvements on top of existing work. In this chapter, we'll be exploring this topic in more detail and, through the lens of clean code, considering how we can individually take actions to make other people's code less of a pain to deal with.
In this chapter, we will cover the following topics:
When we join a new team or take on a new project, we are usually inheriting a large amount of code. Our ability to be productive in these inherited code bases is dependent on our understanding of them. So, before we even seek to make the first change, we need to build in our minds a conceptual model of how things work. It's not necessary for it to be exhaustive and complete, but it must enable us, at a very minimum, to make a change and understand exactly what effect that change may have on all the moving parts of the code base.
Understanding a code base fully is not strictly necessary to make use of it nor to make changes to it, but if we don't have a sufficient understanding of the complexity of all its interrelated parts, then we can fall into a trap. The trap occurs when we, believing we have a good understanding, start making changes. Without understanding the full effects of our actions, we can end up wasting time, implementing things poorly, and producing accidental bugs. Therefore, it is vital that we become properly informed. To do this, we must first gauge how complete or incomplete our view is of the complexity of the system or code base.

Often the things we cannot see are completely unknown to us, and we are therefore unaware that we lack any understanding at all. This is encapsulated by the common expression we don't know what we don't know. It's therefore helpful, when exploring a new code base, to proactively and enthusiastically push to discover and highlight our areas of ignorance. We can do this by following a three-step process:
There are a few specific approaches that are worth employing when it comes to creating and expanding an understanding of a new code base. These include making a flowchart, internalizing the timeline of changes, stepping through the code with a debugger, and confirming your assumptions via tests. We'll explore each of these individually.
One useful method we can employ almost immediately when encountering a new code base is to populate a mind map or flowchart that highlights not only the things we know but the things we aren't yet sure about. Here's a simplified example of such a diagram for a piece of medical software I once worked on:

As you can see, I have tried to outline my current understanding of the user flow and have also added questions or areas of confusion I am personally experiencing in the cloud annotations. Over time, as my understanding grows, I can add to this flowchart.
Imagine you're faced with a large JavaScript application code base that includes several specialized types of views or components. We've been tasked with adding a new drop-down to one of the payment forms within the application. We do a quick search through the code base and identify a number of different dropdown-related components:
They're confusingly named and so we'd like to get a better understanding of them before making changes or utilizing them. To do this, we can just open the source code of each component to establish how it may relate to the others (or how it does not relate).
We end up discovering that TextDropdown and ImageDropdown, for example, both appear to inherit from GenericDropdownComponent:
// TextDropdown.js
class TextDropdown extends GenericDropdownComponent {
//...
}
// ImageDropdown.js
class ImageDropdown extends GenericDropdownComponent {
}
We also observe that both DropdownDataWidget and EnhancedDropdownDataWidget are sub-classes of TextDropdown. The naming of the enhanced drop-down widget might confuse us, and it may be something that we seek to change in the near future, but, for now, we'll need to hold our breath and just work on doing the work we've been tasked with.
By stepping through each dropdown-related source file, we can build up a solid understanding of them without having to make any changes. If the code base employs source control, then we can also blame each file to discover who originally authored it and when. This can inform us how things have changed over time. In our case, we discover the following timeline of changes:

This is incredibly helpful to us. We can see how, originally, there was only one class (named DropdownComponent), which later got changed to GenericDropdownComponent with two sub-classes, TextDropdownComponent and ImageDropdownComponent. Each of these got renamed to TextDropdown and ImageDropdown. Over time, these various changes illuminate the why of how things are at the present time.
When looking at a code base, we often make an implicit assumption that it was created all at once and with complete foresight; however, as our timeline illustrates, the truth is far more complex. Code bases change over time in reaction to new needs. The set of people who work on a code base also changes, and each individual will inevitably have their own way of solving problems. Our acceptance of the slowly evolving nature of every code base will help us to come to terms with its imperfections.
When building an understanding of a singular piece of code within a large application, we can use tooling to debug and study how it functions. In JavaScript, we can simply place a debugger; statement, and then execute the part of the application that we know activates that particular code. We can then step through the code, line by line, to answer the following questions:
Here is an example of doing so in a browser environment (using Chrome Inspector):

Using a debugger like this can present us with a call stack or stack trace, informing us of what path was taken through the code base to get to our debugger; statement. If we are trying to understand how an unfamiliar class or module fits into the larger picture of a code base, this can be very helpful.
One of the best ways to expand our knowledge of unfamiliar code is to write tests to confirm that the code behaves in the way we believe it does. Imagine we are given this piece of obscure code to maintain:
class IssuerOOIDExtractor {
static makeExtractor(issuerInfoInterface) {
return raw => {
const infos = [];
const cleansed = raw
.replace(/[_\-%*]/g, '')
.replace(/\bo(\d+?)\b/g, ($0, id) => {
if (issuerInfoInterface) {
infos.push(issuerInfoInterface.get(id));
}
return `[[ ${id} ]]`;
})
.replace(/^[\s\S]*?(\[\[.+\]\])[\s\S]*$/, '$1');
return { raw, cleansed, data: infos };
};
}
}
This code is only used in a couple of places, but the various inputs are dynamically generated in a difficult-to-debug area of the application. Additionally, there is no documentation and absolutely no tests. It is quite unclear exactly what this code does, but, as we study the code line by line, we can begin to make some basic assumptions and encode these assumptions as assertions. For example, we can plainly see that the makeExtractor static function itself returns a function. We can specify this truth as a test:
describe('IssuerOOIDExtractor.makeExtractor', () => {
it('Creates a function (the extractor)', () => {
expect(typeof IssuerOOIDExtractor.makeExtractor()).toBe('function');
});
});
We can also see some type of regular expression replacement occurring; it seemingly looks for patterns where the letter o is followed by a string of digits (\bo(\d+?)\b). We can begin to explore this extraction functionality by writing a simple assertion in which we give the extractor a string matching that pattern:
const extractor = IssuerOOIDExtractor.makeExtractor();
it('Extracts a single OOID of the form oNNNN', () => {
expect(extractor('o1234')).toEqual({
raw: 'o1234',
cleansed: '[[ 1234 ]]',
data: []
});
});
We can add additional assertions as we slowly discover what the code does. We may never arrive at 100% understanding, but this is OK. Here, we're asserting the fact that the extractor is able to correctly extract multiple OOIDs present within a single string:
it('Extracts multiple OOIDs of the form oNNNN', () => {
expect(extractor('o0012 o0034 o0056 o0078')).toEqual({
raw: 'o0012 o0034 o0056 o0078',
cleansed: '[[ 0012 ]] [[ 0034 ]] [[ 0056 ]] [[ 0078 ]]',
data: []
});
});
When running these tests, we observe the following successful results:
PASS ./IssuerOOIDExtractor.test.js
IssuerOOIDExtractor.makeExtracator
✓ Creates a function (the extractor) (3ms)
The extractor
✓ Extracts a single OOID of the form oNNNN (1ms)
✓ Extracts multiple OOIDs of the form oNNNN (1ms)
Note how we're still not entirely sure what the original code does. We have only scraped the surface, but in doing so, we are building a valuable foundation of understanding that will make it far easier for us to interface with or change this code in the future. With each new successful assertion, we get closer to a complete and accurate understanding of what the code does. And if we commit these assertions as a new test, then we are also improving the test coverage of the code base and providing assistance for future colleagues who may have been similarly confused by the code.
Now that we have a solid grasp of how to explore and understand an inherited piece of code, we can now look into how we might make changes to that code.
Once we have a good level of understanding about an area of a code base, we can begin to make changes. Even at this stage, however, we should be cautious. We are still relatively new to the code base and the system it relates to and so we're probably still unaware of many of its parts. Any change could potentially create unforeseen effects. To move forward we must, therefore, go slowly and considerately, ensuring our code is well designed and well-tested. There are two specific methodologies we should be aware of here:
Let's explore these, one by one.
When changes are needed in an area of the code base that is old or unfamiliar, it can be useful to imagine that you are performing a kind of minimally invasive surgery. The aim in doing this is to maximize the positive effect of a change while minimizing the footprint of the change itself, ensuring not to damage or have too much impact on other parts of the code base. The hope with doing this is that we will be able to produce the necessary changes (upsides) without exposing ourselves too much to the possibility of breakages or bugs (downsides). This is also useful when we aren't sure whether the change is entirely necessary, so we want to only expend minimal effort on it initially.
Let's imagine that we have inherited a GalleryImage component that is responsible for rendering singular images. There are many places in our web application where it is used. The task is to add the ability for videos to be rendered when the URL of the asset indicates that it is a video. The two types of CDN URLs are as follows:
As you can see, there is a clear difference between image and video URLs. This gives us an easy way of differentiating how we render these pieces of media on the page. Ideally, it is reasonable to say that we should implement a new component named GalleryVideo to handle this new type of media. A new component like this would be able to cater uniquely to the problem domain of a video, which is notably different from that of an image. At the very least, a video must be rendered via a <VIDEO> element, while an image must be rendered via <IMG>.
We discover that many of the situations where GalleryImage is used are not well tested and some rely on obscure internal implementation details that would be difficult to discern in bulk (for example, it would be hard to do a find and replace if we wanted to change all GalleryImage usages).
Our available options are as follows:
In ideal circumstances, if we consider the long-term architecture of the code base, it is clear that the first option of creating a new GalleryAsset component is the best one. It gives us a clearly defined abstraction that intuitively caters to the two cases of images and videos and also provides us with the possibility of adding different asset types in the future (for example, audio). It will, however, take longer to implement, and carries quite a significant amount of risk.
The second option is far simpler to implement. In fact, it may only involve the following four-line change set:
@@ -17,6 +17,10 @@ class GalleryImage {
render() {
+ if (/\/VIDEO\//.test(this.props.url)) {
+ return <video src={this.props.url} />;
+ }
+
return <img src={this.props.url} />
}
This is not necessarily a good long-term choice, but it gives us something we can immediately ship to users, fulfilling their needs and the needs of our stakeholders. Once shipped, we can then plan future time to complete the larger necessary change.
To reiterate, the value of a minimally invasive change like this is that it reduces the immediate downsides (risks) to the code base in terms of implementation time and potential breakages. Obviously it is vital to ensure that we are balancing short-term gains with long-term. Often, stakeholders will pressure programmers to implement a change quickly, but if there is no technical dept or reconciliation process, then all of these minimally invasive changes can gather into quite a terrifying beast.
To ensure that the code we change is not too delicate or liable to future regressions, it is wise to write tests alongside them, encoding our expectations.
We've already explored how we can write tests to discover and specify current functionality, and, in previous chapters, we discussed the obvious benefits of following a Test-Driven-Development (TDD) approach. It follows that we should, when operating in an unfamiliar code base, always confirm our changes via cleanly written tests.
In our previous example of introducing the capability of rendering videos to GalleryImage, it would be wise to add a simple test to confirm that <VIDEO> is correctly rendered when the URL contains the "/VIDEO/" substring. This prevents the possibility of future regressions and gives us a strong level of confidence that it works as expected:
import { mount } from 'enzyme';
import GalleryImage from './GalleryImage';
describe('GalleryImage', () => {
it('Renders a <VIDEO> when URL contains "/VIDEO/"', () => {
const rendered = mount(
<GalleryImage url="https://cdn.example.org/VIDEO/1234" />
);
expect(rendered.find('video')).to.have.lengthOf(1);
});
it('Renders a <IMG> when URL contains "/IMAGE/"', () => {
const rendered = mount(
<GalleryImage url="https://cdn.example.org/IMAGE/1234" />
);
expect(rendered.find('img')).to.have.lengthOf(1);
});
});
This is a rather simple test; however, it completely encodes the expectations we have after making our changes. When making small and self-contained changes or larger systemic changes, it's so incredibly valuable to verify and communicate our intent via tests like these. As well as preventing regressions, they aid our colleagues in terms of immediate code review, and the entire team in terms of documentation and general reliability. As such, it's quite normal and preferable to have a team mandate or policy that says you cannot commit a change if it does not come with a test. Enforcing this will, over time, create a code base that produces more reliable functionality for users and is more pleasant to work with for fellow programmers.
We've now completed the section on Inheriting code, and so you should have a good foundational knowledge of how to deal with such a situation. Another challenge in dealing with other people's code is the selection and integration of third-party code, meaning libraries and frameworks. We'll explore this now.
The landscape of JavaScript is filled with a myriad of frameworks and libraries that can ease the burden of implementing all types of functionality. In Chapter 12, Real-World Challenges, we had a look at the difficulties involved in including external dependencies in our JavaScript projects. The modern JavaScript ecosystem provides a rich variety of solutions here, and so dealing with third-party code is far less burdensome than it was before. Nonetheless, the nature of having to interface with this code hasn't really changed. We must still hope that our selected third-party library or framework provides an interface that is intuitive and well-documented, and functionality that fulfills our requirements.
When dealing with third-party code, there are two crucial processes that will define the ongoing risks or benefits we receive. The first is the selection process, where we make a choice as to which library to use, and the second is our integration and adaptation of the library into our code base. We'll now go over both of these in detail.
Picking a library or framework can be a risky decision. Pick the wrong one and it can end up driving much of the architecture of your system. Frameworks are especially notorious for this because, by their nature, they dictate the structure and conceptual underpinning of your architecture. Picking the wrong one and then seeking to change it can be a considerable effort; one that involves changes to almost every single piece of code within an application. As such, it is vital to practice the skill of careful consideration and selection of third-party code:

There are a number of useful considerations we can make in order to help us in the process of selection:
Even using these four criteria, however, we may fall into traps. If you'll recall, in Chapter 3, The Enemies of Clean Code, we discussed the most notable Self (or ego) and The cargo cult. These are also relevant when selecting third-party code. Remember to specifically watch out for the following:
The key thing when making decisions about third-party libraries is to fully appreciate their eventual effects on the code base. The amount of time we sink into making a decision should be proportional to their potential effects. Deciding on a client-side framework for component rendering may be a rather impactful choice as it may prescribe a significant portion of the code base, whereas, for example, a small URL-parsing utility does not have a great impact and can be easily swapped out in the future.
Next, we can discuss how we might integrate and encapsulate a piece of third-party code, following a well-informed selection process.
The downside of picking a third-party abstraction, especially a framework, is that you can end up changing your code base to suit the arbitrary conventions and design decisions of the abstraction's authors. Often, we are made to speak the same language of these third-party interfaces, instead of having them speak our language. Indeed, in many cases, it may be the abstraction's conventions and design that is appealing to us, and so we are more than happy for it to drive the design and nature of our code base. But, in other situations, we may want to be more protected from our chosen abstractions. We may want the option to easily swap them out for other abstractions in the future, or we may already have a set of conventions that we prefer to use.
In such cases, it may be useful to encapsulate these third-party abstractions and deal with them purely through an abstraction layer of our own. Such a layer would typically be called an Adapter:

Very simply, an Adapter will provide an interface that we design, and will then delegate to the third-party abstraction to accomplish its tasks. Imagine if we wished to use a URL-parsing utility called YOORL. We've decided it works perfectly for our needs, and has complete compliance with RFC 3986 (the URI standard). The only issue is that its API is rather burdensome and verbose:
import YOORL from 'yoorl';
YOORL.parse(
new YOORL.URL.String('http://foo.com/abc/?x=123'),
{ parseSearch: true }
).parts();
This would return the following Object:
{
protocol: 'http',
hostname: 'foo.com',
pathname: '/abc',
search: { x: 123 }
}
We would prefer it if the API was far simpler. The length and complexity of the current API, we feel, would expose our code base to needless complexity and risk (the risk of calling it the wrong way, for example). Using an Adapter would allow us to wrap up this non-ideal interface into an interface of our own design:
// URLUtils.js
import YOORL from 'yoorl';
export default {
parse(url) {
return YOORL.parse(
new YOORL.URL.String(url)
).parts();
}
};
This means that any modules within our code base can now interface with this simplified Adapter, insulating them from the unideal API of YOORL:
import URLUtils from './URLUtils';
URLUtils.parse('http://foo.com/abc/?x=123'); // Easy!
Adapters can be thought of as translation mediums, allowing our code base to speak the language of its choice, not having to be slowed down by the arbitrary and inconsistent design decisions of third-party libraries. This not only aids the usability and intuitiveness of the code base but also enables us to very easily make changes to the underlying third-party library without having to change many lines of code at all.
In this chapter, we have explored the tricky topic of other people's code. We've considered how we can deal with legacy code that we inherit; how we can build our understanding of it, how we can debug and make changes without difficult, and how we can confirm our changes with a good testing approach. We've also covered the difficulty of dealing with third-party code, including how to select it and how to interface with it in a risk-averse way via the Adapter pattern. There are plenty of other things that we could have spoken about in this chapter, but hopefully the topics and principles we have been able to explore have given you a sufficient understanding of how to navigate other people's code with an eye toward a clean code base.
In the next chapter, we will cover the topic of communication. It may not appear relevant but communication, both within our workplaces and toward our users, is an absolutely vital skill for the programmer, and without it there is little possibility of clean code. We'll specifically be exploring how to plan and set requirements, how to collaborate and communicate with colleagues, and how to drive change within our projects and workplaces.
We do not write code in isolation. We are embedded in a highly chaotic social world in which we must communicate with other people constantly. Our software itself will, via its interfaces, be part of this communication. Furthermore, if we operate within a team, a workplace, or a community, we are liable to the challenges of effective communication.
The most significant way in which communication has an effect on our code bases is in the setting of requirements and the raising of issues and feedback. Software development is essentially one very elongated feedback process, where every change is precipitated by a communication:

In this chapter, we'll learn how to effectively collaborate and communicate with others, how to plan and set requirements, some common collaboration pitfalls, and their solutions. We'll also learn how to identify and raise larger issues that are preventing us from writing clean JavaScript. Throughout this chapter, we will hopefully begin to appreciate our individually vital roles in the feedback cycle of software development.
In this chapter, we'll see the following topics:
One of the most common communication struggles resides in the process of deciding what to actually build. Programmers will typically spend a lot of time meeting with managers, designers, and other stakeholders to transform a genuine user need into a workable solution. Ideally, this process would be simple: User has [problem]; We create [solution]. End of story! Unfortunately, however, it can be far more complicated.
There are numerous technical constraints and biases of communication that can make even seemingly simple projects turn into punishingly long struggles. This is as relevant to the JavaScript programmer as any other programmer, for we now operate at a level of systemic complexity that was previously only the domain of enterprise programmers wielding Java, C#, or C++. The landscape has changed, and so the humble JavaScript programmer must now be prepared to pick up new skills and ask new questions about the systems they build.
Establishing user needs is vital but it is often taken for granted. It's typical for programmers and other project members to assume they understand a certain user need without really digging into the details, so it's useful to have a process to fall back on. For each ostensible need or problem, we should ensure that we understand the following aspects:
At the beginning of the book, we asked ourselves to consider why we wrote code, and we explored what it means to truly understand the nature of our problem domain. Ideally, we should be able to step inside the shoes of our users, experience the problem domain ourselves, and then craft working solutions from firsthand experience.
Unfortunately, we are not always able to talk directly to our users or walk in their shoes. Instead, we may rely on intermediates such as project managers and designers. And so, we are dependent upon their communication efficacy to relay the user needs to us in a way that allows us to build a correct solution.
Here we see how the needs of our users, combined with the technical and business constraints, flow into an idea that is built into a solution and iterated upon. The translation of User Needs to Idea is vital, as is the process of feedback that allows us to iterate and improve upon our solution:

Since user needs are crucial to the process of development, we have to think carefully about how we balance these with other constraints. It is usually impossible to build the ideal solution, catering well to every single user. Almost every piece of software, whether presented as a GUI or API, is a compromise in which the average user is well catered to, inevitably meaning that the edge case users are left being only partially served by the solution. It's important to consider how we can adequately accommodate as many users' needs as possible, delicately balancing constraints such as time, money, and technical capability.
Following our understanding of user needs, we can begin to design and implement prototypes and models of how a system may work. We'll briefly discuss the process of doing this next.
Software, and especially the web platform, provides us with the benefit of a quick build cycle. We can go from concept to UI in a very short amount of time. This means that ideas can be brought to life during the process of brainstorming, almost in real time. We can then place these prototypes in front of real users, get real feedback, and then iterate quickly towards an optimal solution. Truly, the forte of the web platform—the triad of HTML, CSS, and JavaScript—is that it allows a quick and hacky solution that can be iterated on easily, and can work on multiple platforms and devices:

It's easy to get weighed down by the variety and complexity of JavaScript frameworks and libraries; the sheer burden of them can force us to move at a much slower pace. That's why, when prototyping, it's often better to stick to a simpler stack that you already understand well. If you're accustomed to a framework, or if you are prepared to sink some time into learning, then it is worth utilizing one of many available skeletal boilerplate starter repositories as your starting point. Here are some examples:
These each offer a relatively simple project template that you can use to very quickly set up a new prototype. Even though the tooling used within each one involves multiple build tools and framework options, the setup cost is minimal and so the time it takes to start tackling the real problem domain of your project is kept very short. There are, naturally, similar boilerplates and example applications you can find for server-side Node.js projects, isomorphic web applications, and even robotic or hardware projects.
Now that we've explored the technical process of planning and setting requirements, we can move on to discover some vital communication strategies that'll help us collaborate with others on our code bases.
We intuitively know that communication is vital to an effective project and a clean code base, yet it is annoyingly common to find ourselves in situations such as the following:
These difficulties come about because of cultures and practices of poor communication. This is not only an issue for morale and general fulfillment in our work but can also become a huge issue for the cleanliness of our code bases and the reliability of the technology we build. To foster a clean code base, we must focus on the underlying communication practices that we employ. A good set of communication strategies and practices are incredibly useful in ensuring a clean code base, specifically helping us with the following:
But how do we actually accomplish good communication? We are inherently biased toward our own socialized communication practices, so it can be difficult to change or even see that we have issues with our communication. For this reason, it is useful to identify a set of communication strategies and pitfalls that can re-bias us towards better and higher signal communication.
Whether in online or offline conversations, it is quite easy to fall into a trap where we end up talking over each other instead of to each other. A good and useful conversation is one where the participants are truly listening to each other, instead of merely awaiting their turn to talk.
Consider the following conversation between Person #1 and Person #2:
Here Person #1 is not paying attention to what Person #2 is saying. Instead, they are just continuing with their existing train of thought, reiterating their preference for the React framework. It would be more conducive to good teamwork and a healthier project if Person #1 made an effort to listen to Person #2's points and then respond specifically to them. Compare the preceding conversation with the following one:
Here, Person #1 is being receptive, and not purely talking over Person #2. This shows a much-needed sensitivity and conversational attention. This may seem obvious, or even inane, but you may be surprised how often we end up speaking over each other and the costs it inflicts us with. Consider taking an observational role in your next meeting, observe instances where people fail to properly pay attention, listen, or respond. You may be surprised by its prevalence.
In almost every piece of online or offline communication you have in regards to a code base, the user should be the most important thing. The purpose of our work is to fulfill the expectations of the user and deliver to them a user experience that is intuitive and functional. This is relevant, regardless of whether our end-product is a piece of consumer software or a developer API. The user remains our priority. It is, however, incredibly common to find ourselves in situations where there is a decision to be made and we don't know how to make it; we end up relying on gut instinct or our own biased beliefs. Consider the following:
These may seem like fairly unobjectionable statements, but we should always seek to qualify them from the perspective of the user. If we cannot do this, then there's a strong possibility that the decision holds no water and should be challenged.
For each of the preceding statements, we can defend our reasoning as follows:
Notice how we are expanding upon our of course statements with reasoning that relates specifically to the user. It's easy for us to walk around making assertions about how things should be without actually backing up our claims with strong reasoning. Doing this can lead to pointless and badly argued opposition. It is better to always reason about our decisions from the user's perspective so that, if there is an argument, we are arguing based on what's best for the user and not merely what opinions are most popular, or held most strongly. Always explaining from the perspective of the user also helps to instill a culture where we and our colleagues are constantly thinking about the user, regardless of whether we're programming a deeply specialized API or developing a generic GUI.
Similar in spirit to the single responsibility principle that we use when coding, our communications should ideally only be about one thing at a time. This greatly improves understanding among participants and will ensure that any decisions that are made relate specifically to the matter at hand. Additionally, keeping meetings or communications short ensures that people will be able to pay attention for the entire duration. Long meetings, just like long emails, eventually cause boredom and irritation. And with each topic or tangent added, the chances of each item being individually resolved dwindles massively. It's important to remember this when raising issues and bugs as well. Keep it simple.
There's a tendency, especially in professional environments, to feign a great level of confidence and understanding. This can be to the detriment of knowledge transfer. If everyone is pretending to be masterful, then nobody will take the humble position that's required to learn. It's so valuable to be honest (and even stupid) in our lines of questioning. If we're new additions to a team or are confused about an area of a code base, it's important to ask the questions we truly have so that we can build the understanding necessary to be productive and reliable in our tasks. Without this understanding, we'll flail about, probably causing bugs and other problems. If everyone in a team takes a position of feigned confidence, the team will very quickly become ineffective, with nobody able to resolve their questions or confusions:

This type of questioning we want to aim towards can be called open questioning; a process in which we maximally divulge our ignorances so that we may gain as much understanding as possible in a given area. And similarly to such open questioning, we can say there is also open ideating, wherein we maximally explore and divulge any ideas that we have with the hope of some subset being useful.
Sometimes it's the ideas left unsaid that are the most effective. Generally, if you feel an idea or question is too stupid or wild to say, it's usually a good idea to say it. The worst-case scenario (the downside) is that it is an inapplicable or obvious question or idea. But the best-case scenario (the upside) is that you've either gained understanding, asked a question that many people had on their minds (and thus aided their understanding), or have come up with an idea that drastically transforms the efficacy of the team or the quality of the code base. The upsides of being open are assuredly worth the downsides.
Much of a programmer's time is taken up by the isolated pursuit of writing code. This is, to many programmers, their ideal situation; they are able to block out the rest of the world and find fluid productivity, writing logic with speed and fluency. One risk of this isolation, however, is that vital knowledge of a code base or system can accrue in the minds of the few. Without being distributed, there is a risk that the code base will become increasingly specialized and complex, limiting the ability of newcomers and colleagues to navigate it with ease. For this reason, it is essential to consider how to transfer knowledge effectively between programmers.
As discussed previously in the book, we already have a number of formal ways to transfer knowledge regarding a piece of code:
Even though these mediums, if built correctly, can be effective in relaying knowledge, there appears to always be a need for something else. The basic human convention of ad hoc communication is a method that has stood the test of time and still remains one of the most effective methods.
One of the best ways to learn about a new code base is through pair programming, an activity in which you sit alongside a more experienced programmer and collaborate together on bug fixes or feature implementations. This is especially useful for the unfamiliar programmer, as they are able to benefit from the existing knowledge and experience of their programming partner. Pair programming is also useful when there is an especially complex issue to solve. Having two or more brains tackling the problem can drastically increase problem-solving ability and limit the possibility of bugs.
Even outside of pair programming, generally having a Q&A or teacher-student dynamic can be very useful. Setting aside time to talk to individuals who have the knowledge you desire and asking them pointed but exploratory questions will usually yield a lot of understanding. Do not underestimate the power of a focused conversation with someone that has the knowledge you desire.
A large part of being a programmer is identifying issues and fixing them. As part of our work, we employ many different moving parts, many of which will be maintained by other teams or individuals, and as such, we'll need to be effective in identifying and raising issues with code and systems that we don't have a full understanding of. Much like anything we do as programmers, the way in which we articulate these issues must take into consideration the target audience (user) of the issue or bug report that we're expressing. When we begin to see these pieces of communication as user experiences in their own right, we'll start to be genuinely effective communicators.
Raising bugs is a skill. It can be done poorly or effectively. To illustrate this, let's consider two issues on GitHub. Each of them raise the same issue but do so in drastically different ways. This is the first variant:

This is the second variant:

As a maintainer of this code base, which bug report would you prefer to receive? Obviously the second. Yet we see, time and time again, thousands of bug reports and raised issues on open source projects that not only fail to accurately relay the issue at hand but are impatiently worded and disrespectful of the time and efforts of the project owner.
Generally, when raising a bug it is best to include, at minimum, the following information:
Here's an example of such a bug report for a fictional sum() function:
It may also be useful to include information about the environment in which the code is running, including hardware and software (for example, MacBook 2013 Retina, Chrome version 43.01). The entire purpose of raising a bug is to communicate an unexpected or incorrect behavior with a level of accuracy and detail that'll allow a swift resolution. If we limit the amount of information we provide, or are outright rude, we drastically decrease the probability of our issue being resolved.
Apart from the specific steps we should take when raising issues, there is also a wider question around how we should drive and inspire systemic change in a piece of software or a culture. We'll be exploring this next.
A bug is usually considered a self-contained technical issue with a piece of hardware or software. There are, however, larger or more systemic issues that we face every day, and these can be expressed in terms of a culture or in terms of the everyday conventions and patterns that we employ throughout a system. Here are some fictional examples of issues from within a typical IT consultancy:
These issues are slightly too broad or subjective to be expressed as definitive bugs, so we'll need to explore other means to surface them and get them resolved. It may be useful to think of such systemic issues as opportunities for growth instead of bugs, as this can vastly affect how on-board people are with your proposed changes.
Broadly, the steps involved in creating systemic change are as follows:
One of the traps in creating systemic change is to wait too long or to be too cautious in approaching the problem. Gaining feedback from others is really valuable, but it is not necessary to depend entirely upon their validation. It's sometimes hard for people to step outside their perspective and see certain issues, especially if they're very accustomed to how things are currently done. Instead of waiting for them to see things your way, it may be best to go ahead with an isolated version of your proposed solution and later prove its efficacy to them.
Many of the things we wish to change within the technologies and systems we work with every day are not easily solved. They may be complex, unwieldy, and often multi-disciplinary problems. Examples of these types of problems are easily found on discussion forums and community feedback surrounding standards iteration, such as with the ECMAScript language specification. Rarely is an addition or change to the language accomplished simply. Patience, consideration, and communication are all needed to solve these problems and move ourselves and our technologies forward.
In this chapter, we have tried to explore the challenge of effective communication in a technical context, and have broadly discussed the communicative process involved in taking a problem from the ideation stage to the prototype stage. We have also covered the task of communicating and advocating for technological change, whether in the form of bug reports or raising broader issues concerning systemic problems. Programmers are not just the authors of code; they operate as part of the systems they are building, as crucial agents in the iterative feedback cycles that result in clean software. Understanding the considerable roles we play in these systems and feedback cycles is hugely empowering and begins to get to the crux of what it means to be a clean JavaScript programmer.
In the next and final chapter, we will be bringing together everything we have learned in the book so far, exploring a new problem domain via a case study. That'll conclude our exploration into clean code in JavaScript.
In this book, we have discussed a litany of principles, walked through almost every aspect of the JavaScript language, and have discussed, at length, what constitutes clean code. This has all been working toward a final destination where we are fully equipped to write beautiful and clean JavaScript code that tackles real and challenging problem domains. The pursuit of clean code, however, is never complete; new challenges will always arise that make us think in new and paradigm-shifting ways about the code we write.
In this chapter, we'll be walking through the process of creating a new piece of functionality in JavaScript. This will involve both client-side and server-side parts, and will force us to apply many of the principles and knowledge we've gathered throughout the book. The specific problem we'll be tackling has been adapted from a real-life project that I was responsible for, and while we won't be going into every nook and cranny of its implementation, we will be covering the most important parts. The completed project is available for you to view on GitHub at the following link: https://github.com/PacktPublishing/Clean-Code-in-JavaScript.
In this chapter, we're going to cover the following topics:
The problem we'll be solving relates to a core part of our web application's user experience. The web application we'll be working on is a frontend to a large plant database with tens of thousands of different species of plants. Among other functionality, it allows users to find specific plants and add them to collections so that they can keep track of their exotic greenhouses and botanical research inventories. The illustration is shown as follows:

Currently, when users wish to find a plant, they must use a search facility that involves entering a plant name (the full Latin name) into a text field, clicking Search, and receiving a set of results, as shown in the following screenshot:

This works well enough, but following some user focus groups and online feedback, it has been decided that we need to offer a better UX for users that enables them to more quickly find the plants they're interested in. Specific points that were raised are as follows:
There are a number of usability concerns expressed here. We can distill them into the following three topics:
The task is now becoming clearer. We need to improve the UX so that users are able to query the database of plants in a way that is faster, provides more immediate feedback, and lets them prevent or correct typing errors along the way.
After some brainstorming, we decided that we can solve our problem in quite a conventional way; we can simply transform the input field into one that provides an auto-suggestion dropdown. Here's a mockup:

This auto-suggestion dropdown would have the following characteristics:
These are the core behaviors of our component, and in order to implement them, we'll need to consider both client-side and server-side parts. Our client will have to render <input> to the user, and as they type, it will have to dynamically adjust the list of suggestions. The server will have to provide to the client a list of suggestions for each potential query, while taking into consideration the fact that results will need to be delivered quickly. Any significant latency will drastically reduce the benefit of the user experience that we're trying to create.
It just so happens that this new Plant Selection component will be the first piece of significant client-side code within our web application, and as such, it's important to note that our design decisions will impact not only this specific component but also any other components we consider building in the future.
To aid us in our implementation, and considering the possibility of other potential additions in the near future, we've decided to adopt a JavaScript library to assist in the manipulation of the DOM, and a supporting toolset that enables us to work swiftly and to a high-level of quality. In this instance, we've decided to use React on the client side, with webpack and Babel to aid in compilation and bundling, and Express on the server side for HTTP routing.
As discussed, we've decided to build our Plant Selection functionality as its own self-contained application with both a client (the React component) and a server (the plant-data API). Having this level of isolation allows us to focus purely on the problem of selecting plants, but there's no reason that this couldn't be integrated into a larger code base at a later time.
Our directory structure is roughly as follows:
EveryPlantSelectionApp/
├── server/
│ ├── package.json
| ├── babel.config.js
│ ├── index.js
| └── plantData/
│ ├── plantData.js
│ ├── plantData.test.js
| └── data.json
└── client/
├── package.json
├── webpack.config.js
├── babel.config.js
├── app/
| ├── index.jsx
| └── components/
| └── PlantSelectionInput/
└── dist/
├── main.js (bundling target)
└── index.html
In addition to reducing complexity for us (the programmers) the separation of server and client means that the server-side application (that is, the Plant Selection API) can be run on its own distinct server if necessary, while the client can be served up statically from a CDN, requiring only the server-side's address in order to access its REST API.
The server of EveryPlantSelectionApp is responsible for retrieving the plant names (the plant families, genuses, and species) and making them available to our client-side code via a simple REST API. To do this, we can use the express Node.js library, which enables us to route HTTP requests to specific functions, easily delivering JSON to our client.
Here's the skeletal beginnings of our server implementation:
import express from 'express';
const app = express();
const port = process.env.PORT || 3000;
app.get('/plants/:query', (req, res) => {
req.params.query; // => The query
res.json({
fakeData: 'We can later place some real data here...'
});
});
app.listen(
port,
() => console.log(`App listening on port ${port}!`)
);
As you can see, we're implementing just one route (/plants/:query). This will be requested by the client whenever a user enters a partial plant name into the <input/>, so that a user typing Carduaceus may produce the following set of requests to the server:
GET /plants/c
GET /plants/ca
GET /plants/car
GET /plants/card
GET /plants/cardu
GET /plants/cardua
...
You can imagine how this may result in a larger number of expensive and possibly redundant requests, especially if a user is typing quickly. It's possible that a user will type cardua before any of the previous requests can complete. For that reason, when we come around to implementing the client side, it'll be appropriate for us to use some kind of request throttling (or request debouncing) to ensure that we're only making a reasonable number of requests.
In order to implement the /plants/ endpoint, we need to consider the most optimal way to search through the names of over 300,000 different plant species for matches. To accomplish this, we'll be using a special in-memory data structure called a trie. This is also known as a prefix tree and is very common to use in situations where autosuggestion or autocompletion needs to occur.
A trie is a tree-like structure that stores chunks of letters that appear next to each other as a series of nodes attached by branches. It's much easier to visualize than to describe, so let's imagine that we need a trie based on the following data:
['APPLE', 'ACORN', 'APP', 'APPLICATION']
Using that data, the produced trie might look something like this:

As you can see, our dataset of four words has been represented as a tree-like structure where the first common letter, "A", serves as the root. The "CORN" suffix branches off from this. Additionally, the "PP" branch (forming "APP"), branches off, and the last "P" of that then branches off to "L", which itself then branches off to "E" (forming "APPLE") and "ICATION" (forming "APPLICATION").
This may seem convoluted, but given this trie structure, we can, given an initial prefix typed by a user like "APPL", easily find all matching words ("APPLE" and "APPLICATION") by simply stepping through the nodes of the tree. This is far more performant than any linear search algorithm. For our purposes, given a prefix of a plant name, we want to be able to performantly display every plant name that the prefix may lead to.
Our specific dataset will include over 300,000 different plant species, but for the purposes of this case study, we'll only be using species from the Acanthaceae family, which amounts to around 8,000 species. These are available to use in the form of JSON as follows:
[
{ id: 105,
family: 'Acanthaceae',
genus: 'Andrographis',
species: 'alata' },
{ id: 106,
family: 'Acanthaceae',
genus: 'Justicia',
species: 'alata' },
{ id: 107,
family: 'Acanthaceae',
genus: 'Pararuellia',
species: 'alata' },
{ id: 108,
family: 'Acanthaceae',
genus: 'Thunbergia',
species: 'alata' },
// ...
]
We'll be feeding this data into a third-party trie implementation called trie-search on NPM. This package has been selected because it fulfills our requirements and seems like a well-tested and well-maintained library.
In order for the trie to operate as we desire, we'll need to concatenate the family, genus, and species of each plant into a singular string. This enables the trie to include both the fully qualified plant name (for example, "Acanthaceae Pararuellia alata") and the split names (["Acanthaceae", "Pararuellia", "alata"]). The split name is automatically generated by the trie implementation we're using (meaning it splits strings on whitespace, via the regex /\s/g):
const trie = new TrieSearch(['name'], {
ignoreCase: true // Make it case-insensitive
});
trie.addAll(
data.map(({ family, genus, species, id }) => {
return { name: family + ' ' + genus + ' ' + species, id };
})
);
The preceding code enters our dataset into the trie. Following this, it can be queried by simply passing a prefix string to its get(...) method:
trie.get('laxi');
Such a query (for the prefix, laxi) would return the following from our dataset:
[
{ id: 203,
name: 'Acanthaceae Acanthopale laxiflora' },
{ id: 809,
name: 'Acanthaceae Andrographis laxiflora' },
{ id: 390,
name: 'Acanthaceae Isoglossa laxiflora' },
//... (many more)
]
So, with regard to our REST endpoint, /photos/:query, all it needs to do is return a JSON payload that contains whatever we get from trie.get(query):
app.get('/plants/:query', (req, res) => {
const queryString = req.params.query;
if (queryString.length < 3) {
return res.json([]);
}
res.json(
trie.get(queryString)
);
});
To separate our concerns a little better and to ensure we're not mixing too many different layers of abstraction (in possible violation of The Law of Demeter), we can abstract away our trie data structure and plant data to a module of its own. We can call this plantData to communicate the fact that it encapsulates and provides access to the plant data. The nature of how it works, which happens to be via an in-memory trie data structure, does not need to be known to its consumers:
// server/plantData.js
import TrieSearch from 'trie-search';
import plantData from './data.json';
const MIN_QUERY_LENGTH = 3;
const trie = new TrieSearch(['fullyQualifiedName'], {
ignoreCase: true
});
trie.addAll(
plantData.map(plant => {
return {
...plant,
fullyQualifiedName:
`${plant.family} ${plant.genus} ${plant.species}`
};
})
);
export default {
query(partialString) {
if (partialString.length < MIN_QUERY_LENGTH) {
return [];
}
return trie.get(partialString);
}
};
As you can see, this module returns an interface that provides one method, query(), which our main HTTP routing code can utilize to deliver the JSON result for /plants/:query:
//...
import plantData from './plantData';
//...
app.get('/plants/:query', (req, res) => {
const query = req.params.query;
res.json( plantData.query(partial) );
});
Because we have isolated and contained the plant-querying functionality, it is now far easier to make assertions about it. Writing some tests that target the plantData abstraction will give us a high level of confidence that our HTTP layer is using a reliable abstraction, minimizing the potential bugs that can crop up within our HTTP layer itself.
We can write tests for our plantData module in a file intuitively located alongside it and named plantData.test.js:
import plantData from './plantData';
describe('plantData', () => {
describe('Family+Genus name search (Acanthaceae Thunbergia)', () => {
it('Returns plants with family and genus of "Acanthaceae Thunbergia"', () =>{
const results = plantData.query('Acanthaceae Thunbergia');
expect(results.length).toBeGreaterThan(0);
expect(
results.filter(plant =>
plant.family === 'Acanthaceae' &&
plant.genus === 'Thunbergia'
)
).toHaveLength(results.length);
});
});
});
As you can see, this test is asserting whether an Acanthaceae Thunbergia query intuitively returns plants that have a fully qualified name containing these terms. In our dataset, this will only include plants that have an Acanthaceae family and a Thunbergia genus, so we can simply confirm that the results match that expectation. We can also check that partial searches, such as Acantu Thun, also intuitively return any plants that have either family, genus, or species names beginning with Acantu or Thun:
describe('Partial family & genus name search (Acantu Thun)', () => {
it('Returns plants that have a fully-qualified name containing both "Acantu" and "Thunbe"', () => {
const results = plantData.query('Acant Thun');
expect(results.length).toBeGreaterThan(0);
expect(
results.filter(plant =>
/\bAcant/i.test(plant.fullyQualifiedName) &&
/\bThun/i.test(plant.fullyQualifiedName)
)
).toHaveLength(results.length);
});
});
We confirm our expectations here by asserting that every returned result's fullyQualifiedName matches the regular /\bAcant/i and /\bThun/i expressions. The /i expression indicates case sensitivity. The \b expression here represents a word boundary so that we can ensure that the Acant and Thun substrings appear at the beginning of individual words and are not embedded within words. For example, imagine a plant called Luathunder. We don't want our autosuggestion mechanism to match such instances. We only want it to match prefixes, as that is how users will be entering plant families, genuses, or species into <input /> (from the start of each word).
Now that we have a well-tested and isolated server-side architecture, we can begin to move onto the client side, where we will be rendering the plant names provided by /plants/:query in response to the user typing.
Our first step, on the client, is to introduce React and a supporting toolset that can aid us in development. In the old days of web development, it was, and arguably still is, entirely possible to build things without complicated tools and build steps. In times past, we were able to simply create an HTML page, include any third-party dependencies inline, and then begin writing our JavaScript without having to worry about anything else:
<body>
... Content
<script src="//example.org/libraryFoo.js"></script>
<script src="//example.org/libraryBaz.js"></script>
<script>
// Our JavaScript code...
</script>
</body>
Technically we can still do this. Even when using modern frontend frameworks such as React, we could opt to just include it as a <script> dependency and then write vanilla JavaScript inline. However, by doing this, we would not be receiving the following advantages:
Fundamentally, the very nature of web applications is more complicated now, especially on the frontend. For our purposes of creating an autosuggestion component, we need to ensure that we've got a good foundation of tools and build steps so that ongoing development can be seamless and simple. This can create a headache when setting things up but is worth it in the long run.
In order to compile our JavaScript (including React's JSX), we'll be using Babel, which can take our JavaScript and convert it into widely supported regular JavaScript syntax. To add Babel as a dependency within EveryPlantSelectionApp/client, we can use npm to install it and its various preset configurations:
# Install babel's core dependencies:
npm install --save-dev @babel/core @babel/cli
# Install some smart presets for Babel, allowing us to not have
# to worry about which specific JS syntax we're using:
npm install --save-dev @babel/preset-env
# Install a smart preset for React (i.e. JSX) usage:
npm install --save-dev @babel/preset-react
Babel will manage the compilation of our JavaScript to a syntax that is widely supported. But in order to make these files ready for delivery to a browser, we need to bundle them into a singular file that can be delivered by itself within our HTML like so:
<script src="./ourBundledJavaScript.js"></script>
To accomplish this, we will need to use a bundler, such as webpack. Webpack can carry out the following tasks for us:
In order to use webpack, we need to install several related dependencies:
# Install Webpack and its CLI:
npm install --save-dev webpack webpack-cli
# Install Webpack's development server, which enables us to more easily
# develop without having to keep re-running the build process:
npm install --save-dev webpack-dev-server
# Install a couple of helpful packages that make it easier for
# Webpack to make use of Babel:
npm install --save-dev babel-loader babel-preset-react
Webpack also requires its own configuration file, named webpack.config.js. Within this file, we must tell it how to bundle our code and whereabouts in our project we want the bundled code to output to:
const path = require('path');
module.exports = {
entry: './app/index.jsx',
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/react']
}
}
}
]
},
devServer: {
contentBase: path.join(__dirname, 'dist'),
compress: true,
port: 9000
},
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
}
};
This configuration is essentially telling webpack the following:
Lastly, we need to install React so that we're ready to create our plant selection component:
npm install --save react react-dom
It may seem like this is a lot of work just to render a basic UI component, but what we've actually done is created a foundation upon which we can accommodate many new features, and we've created a build pipeline that will make it easier to ship our development code base to production.
Our component's job is to display an enhanced <input> element that will, when focused, react to what the user types by rendering a dropdown-style list of available options that the user can then select from.
As a primitive outline, we can imagine the component as containing <div>, <input> into which the user can type, and <ol> to display the suggestions:
const PlantSelectionInput = () => {
return (
<div className="PlantSelectionInput">
<input
autoComplete="off"
aria-autocomplete="inline"
role="combobox" />
<ol>
<li>A plant name...</li>
<li>A plant name...</li>
<li>A plant name...</li>
</ol>
</div>
);
};
We only want <ol> to display when <input> is focused. In order to accomplish this, we'll need to bind to both the focus ad blur events of <input> and then create a distinct piece of state that can track whether we should consider the component open or not. We can call this piece of state isOpen, and we can conditionally render or not render <ol> based on its Boolean value:
const PlantSelectionInput = () => {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="PlantSelectionInput">
<input
onFocus={() => setIsOpen(true)}
onBlur={() => setIsOpen(false)}
autoComplete="off"
aria-autocomplete="inline"
role="combobox" />
{
isOpen &&
<ol>
<li>A plant name...</li>
<li>A plant name...</li>
<li>A plant name...</li>
</ol>
}
</div>
);
};
The next step is for us to bind to the change event of <input> so that we can take whatever the user has typed and trigger a request to our /plants/:query endpoint in order to discern what suggestions to show the user. First, however, we want to create a mechanism via which the request can occur. In the React world, it suggests modeling this functionality as a Hook of its own. Remembering that Hooks are, by convention, prefixed with a use verb, we could call this something like usePlantLike. As its sole argument, it can accept a query field (the string typed by the user), it can return an object with a loading field (to indicate the current loading state) and a plants field (to contain the suggestions):
// Example of calling usePlantsLike:
const {loading, plants} = usePlantsLike('Acantha');
Our implementation of usePlantsLike is thankfully quite simple:
// usePlantLike.js
import {useState, useEffect} from 'react';
export default (query) => {
const [loading, setLoading] = useState(false);
const [plants, setPlants] = useState([]);
useEffect(() => {
setLoading(true);
fetch(`/plants/${query}`)
.then(response => response.json())
.then(data => {
setLoading(false);
setPlants(data);
});
}, [query]);
return { loading, plants };
};
Here, we are using another React state management pattern, useEffect(), to run a specific function whenever the query argument changes. So, if usePlantLike receives a new query argument, for example, Acantha, then the loading state will be set to true and a new fetch() will be instigated, the result of which will populate the plants state. This can be difficult to wrap one's head around, but for the purposes of the case study, all we really need to appreciate is the fact that this usePlantsLike abstraction is encapsulating the complexity of issuing the /plants/:query requests to the server.
We can now use our new React Hook whenever the user types something into <input>. To do this, we can bind to its change event and every time it's triggered, grab its value, and then pass it as the query argument to usePlantsLike in order to derive a new set of suggestions for the user. These can then be rendered within our <ol> container:
const PlantSelectionInput = ({ isInitiallyOpen, value }) => {
const inputRef = useRef();
const [isOpen, setIsOpen] = useState(isInitiallyOpen || false);
const [query, setQuery] = useState(value);
const {loading, plants} = usePlantsLike(query);
return (
<div className="PlantSelectionInput">
<input
ref={inputRef}
onFocus={() => setIsOpen(true)}
onBlur={() => setIsOpen(false)}
onChange={() => setQuery(inputRef.current.value)}
autoComplete="off"
aria-autocomplete="inline"
role="combobox"
value={value} />
{
isOpen &&
<ol>{
plants.map(plant =>
<li key={plant.id}>{plant.fullyQualifiedName}</li>
)
}</ol>
}
</div>
);
};
Here, we've added a new piece of state, query, which we set via setQuery within the onChange handler of <input>. This query mutation will then cause usePlantsLike to issue a new request from the server and populate <ol> with multiple <li> elements, each representing an individual plant name suggestion.
And with that, we have completed the basic implementation of our component. In order to make use of it, we can render it in our client/index.jsx entry point:
import ReactDOM from 'react-dom';
import React from 'react';
import PlantSelectionInput from './components/PlantSelectionInput';
ReactDOM.render(
<PlantSelectionInput />,
document.getElementById('root')
);
This code attempts to render <PlantSelectionInput/> to an element with a "root" ID. As outlined previously, webpack, our bundling tool, will automatically bundle our compiled JavaScript into a singular main.js file and place it in dist/ (that is, distribution) directory. This will sit alongside our index.html file, which will serve as a user-facing portal to our application. For our purposes, this only needs to be a simple page that demonstrates PlantSelectionInput:
<!DOCTYPE html>
<html>
<head>
<title>EveryPlant Selection App</title>
<style>
/* our styles... */
</style>
</head>
<body>
<div id="root"></div>
<script src="./main.js"></script>
</body>
</html>
We can place any relevant CSS within the <style> tag here in index.html:
<style>
.PlantSelectionInput {
width: 100%;
display: flex;
position: relative;
}
.PlantSelectionInput input {
background: #fff;
font-size: 1em;
flex: 1 1;
padding: .5em;
outline: none;
}
/* ... more styles here ... */
</style>
The styling of our component is not particularly challenging as it is just a list of textual items. The main challenge is in ensuring that, when the component is in its fully opened state, the list of suggestions appears atop any other content on the page. This can be achieved by relatively positioning the <input> container and then absolutely positioning <ol>, visualized here:

This concludes the implementation of our component, but we should also implement a basic level of testing (at least). To accomplish this, we'll be using Jest, a testing library, and its snapshot matching functionality. This will enable us to confirm that our React component produces the expected hierarchy of DOM elements and will protect us from future regressions:
// PlantSelectionInput.test.jsx
import React from 'react';
import renderer from 'react-test-renderer';
import PlantSelectionInput from './';
describe('PlantSelectionInput', () => {
it('Should render deterministically to its snapshot', () => {
expect(
renderer
.create(<PlantSelectionInput />)
.toJSON()
).toMatchSnapshot();
});
describe('With configured isInitiallyOpen & value properties', () => {
it('Should render deterministically to its snapshot', () => {
expect(
renderer
.create(
<PlantSelectionInput
isInitiallyOpen={true}
value="Example..."
/>
)
.toJSON()
).toMatchSnapshot();
});
});
});
Jest helpfully saves the produced snapshots to a __snapshots__ directory and then compares any future executions of the tests against these saved snapshots. In addition to these tests, we'll also be able to implement regular functional, or even E2E tests that can encode expectations such as When the user types, the list of suggestions updates correspondingly.
This concludes our construction of the component and our case study. If you have a look at our GitHub repository, you can see the completed project, play with the component, run the tests yourself, and you can fork the repository to make your own changes too.
Here's the link to the GitHub repository: https://github.com/PacktPublishing/Clean-Code-in-JavaScript.
In this, the final chapter, we have explored a real-world problem through the lens of the principles and learnings that we have gathered throughout the book. We posed a problem that users were encountering and then designed and implemented a user experience that solved their problem in a clean way. This included both server-side and client-side pieces, enabling us to see, from start to finish, what a self-contained JavaScript project may look like. Although we haven't been able to cover every single detail, I hope that this chapter has been helpful in cementing the core ideas behind clean code and that you now feel better prepared to write clean JavaScript code to tackle all types of problem domains. One core tenet I hope you can take away with you is simply this: focus on the user.
If you enjoyed this book, you may be interested in these other books by Packt:
Building Forms with Vue.js
Marina Mosti
ISBN: 978-1-83921-333-5
Web Development with Angular and Bootstrap - Third Edition
Sridhar Rao Chivukula, Aki Iskandar
ISBN: 978-1-78883-810-8
Please share your thoughts on this book with others by leaving a review on the site that you bought it from. If you purchased the book from Amazon, please leave us an honest review on this book's Amazon page. This is vital so that other potential readers can see and use your unbiased opinion to make purchasing decisions, we can understand what our customers think about our products, and our authors can see your feedback on the title that they have worked with Packt to create. It will only take a few minutes of your time, but is valuable to other potential customers, our authors, and Packt. Thank you!