CHAPTER FOUR
Too Much Rope, or JavaScript for Teams
Beauty is power and elegance, right action, form fitting function, intelligence, and reasonability.
Kim Stanley Robinson, Red Mars
JavaScript is a flexible language. In fact, this entire book is a testament to its expressiveness and dynamism. Within these pages you’ll hear stories of how to bend the language to your will, descriptions of how to use it to experiment and play, and suggestions for seemingly contradictory ways to write it.
My job is to tell a more cautionary tale.
I’m here to ask the question: what does it mean to write JavaScript in a team? How do you maintain sanity with 5, 10, 100 people committing to the same codebase? How do you make sure new team members can orient themselves quickly? How do you keep things DRY without forcing broken abstractions?
Know Your Audience
In 2005 I joined the Gmail team in sunny Mountain View, California. The team was building what many considered at the time to be the pinnacle of web applications. They were awesomely smart and talented, but across Google, JavaScript wasn’t considered a “real programming language”—you engineered backends, you didn’t engineer web UIs—and this mentality affected how they thought about the code.
Furthermore, even though the language was 10 years old, JavaScript engines were still limited: they were designed for basic form validation, not building applications. Gmail was starting to hit performance bottlenecks. To get around these limitations much of the application was implemented as global functions, anything requiring a dot lookup was avoided, sparse arrays were used in place of templates, and string concatenation was a no-no.
The team was writing first and foremost for the JavaScript engine, not for themselves or others. This led to a codebase that was hard to follow, inconsistent, and sprawling.
Instead of optimizing by hand, we transitioned to a world where code was written for humans and the machine did the optimizations. This wasn’t a new language, mind you—it was important that the raw code be valid JavaScript, for ease of understanding, testing, and interoperability. Using the precursor to the Closure Compiler, we developed optimization passes that would collapse namespaces, optimize strings, inline functions, and remove dead code. This is work much better suited to a machine, and it allowed the raw code to be more readable and more maintainable.
TIP
Lesson 1: Code for one another, and use tools to perform mechanical optimizations.
Stupid Good
As the old adage goes, debugging is harder than writing code, so if you write the cleverest code you can, you’ll never be clever enough to debug it.
It can be fun to come up with obscure and arcane ways of solving problems, especially since JavaScript gives you so much flexibility. But save it for personal projects and JavaScript puzzlers.
When working in a team you need to write code that everyone is going to understand. Some parts of the codebase may go unseen for months, until a day comes when you need to debug a production issue. Or perhaps you have a new hire with little JavaScript experience. In these types of situation, keeping code simple and easy to understand will be better for everyone. You don’t want to spend time decoding some bizarro, magical incantation at two in the morning while debugging production issues.
Consider the following:
var
el
=
document
.
querySelector
(
'.profile'
);
el
.
classList
[[
'add'
,
'remove'
][
+
el
.
classList
.
contains
(
'on'
)]](
'on'
);
And an alternative way of expressing the same behavior:
var
el
=
document
.
querySelector
(
'.profile'
);
if
(
el
.
classList
.
contains
(
'on'
))
el
.
classList
.
remove
(
'on'
);
else
el
.
classList
.
add
(
'on'
);
Saying that the second snippet is better than the first may seem in conflict with the concept that “succinctness = power.” But I believe there is a disconnect that stems from the common synonyms for succinct: compact, brief.
I prefer terse as a synonym:
using few words, devoid of superfluity, smoothly elegant
The first snippet is more compact than the second snippet, but it is denser and actually includes more symbols. When reading the first snippet you have to know how coercion rules apply when using a numeric operator on a Boolean, you have to know that methods can be invoked using subscript notation, and you have to notice that square brackets are used for both defining an array literal and method lookup.
The second snippet, while longer, actually has less syntax for the reader to process. Furthermore, it reads like English: “If the element’s class list contains ‘on’, then remove ‘on’ from the class list; otherwise, add ‘on’ to the class list.”
All that said, an even better solution would be to abstract this functionality and have the very simple, readable, and succinct:
toggleCssClass
(
document
.
querySelector
(
'.profile'
),
'on'
);
TIP
Lesson 2: Keep it simple; compactness != succinctness.
Keep It Classy
When I’m talking with “proper programmers,” they often complain about how terrible JavaScript is. I usually respond that JavaScript is misunderstood, and that one of the main issues is that it gives you too much rope—so inevitably you end up hanging yourself.
There were certainly questionable design decisions in the language, and it is true that the early engines were quite terrible, but many of the problems that occur as JavaScript codebases scale can be solved with pretty standard computer science best practices. A lot of it comes down to code organization and encapsulation.
Unfortunately, until we finally get ES6 we have no standard module system, no standard packaging mechanisms, and a prototypal inheritance model that confuses a lot of people and begets a million different class libraries.
While JavaScript’s prototypal inheritance allows instance-based inheritance, I generally suggest when working in a team that you simulate classical inheritance as much as possible, while still utilizing the prototype chain. Let’s consider an example:
var
log
=
console
.
log
.
bind
(
console
);
var
bob
=
{
money
:
100
,
toString
:
function
()
{
return
'$'
+
this
.
money
}
};
var
billy
=
Object
.
create
(
bob
);
log
(
'bob:'
+
bob
,
'billy:'
+
billy
);
// bob:$100 billy:$100
bob
.
money
=
150
;
log
(
'bob:'
+
bob
,
'billy:'
+
billy
);
// bob:$150 billy:$150
billy
.
money
=
50
;
log
(
'bob:'
+
bob
,
'billy:'
+
billy
);
// bob:$150 billy:$50
delete
billy
.
money
;
log
(
'bob:'
+
bob
,
'billy:'
+
billy
);
// bob:$150 billy:$150
In this example, billy
inherits from bob
. What that means in
practice is that billy.prototype = bob
, and nonmatching property
lookups on billy
will delegate to bob
. In other words, to begin
with billy
’s $100 is bob
’s $100; billy
isn’t a copy of bob
. Then, when billy
gets his own money, it essentially overrides the
property that was being inherited from bob
. Deleting billy
’s money
doesn’t set it to undefined
; instead, bob
’s money becomes billy
’s
again.
This can be rather confusing to newcomers. In fact, developers can go a long time without ever knowing precisely how prototypes work. So, if you use a model that simulates classical inheritance, it increases the chances that people on your team will get on board quickly and allows them to be productive without necessarily needing to understand the details of the language.
Both the Closure library’s goog.inherits
and Node.js’s
util.inherits
make it easy to write class-like structures while still
relying on the prototype for wiring:
function
Bank
(
initialMoney
)
{
EventEmitter
.
call
(
this
);
this
.
money
=
money
;
}
util
.
inherits
(
Bank
,
EventEmitter
);
Bank
.
prototype
.
withdraw
=
function
(
amount
)
{
if
(
amount
<=
this
.
money
)
{
this
.
money
-=
amount
;
this
.
emit
(
'balance_changed'
,
this
.
money
);
// inherited
return
true
;
}
else
{
return
false
;
}
}
This looks very similar to inheritance in other languages.
Bank
inherits from EventEmitter
; the superclass’s constructor is
called in the context of the new instance; util.inherits
wires up the
prototype chain just like we saw with bob
and billy
earlier; and then
the property lookup for emit
falls to the EventEmitter
“class.”
A suggested exercise for the reader is to create instances of a class
without using the new
keyword.
TIP
Lesson 3: Just because you can doesn’t mean you should.
TIP
Lesson 4: Utilize familiar paradigms and patterns.
Style Rules
The need for consistent style as codebases and teams grow is nothing unique to JavaScript. However, where many languages are opinionated about coding style, JavaScript is lenient and forgiving. This means it’s all the more important to define a set of rules the team should stick to.
Good style is subjective and can be difficult to define, but there are many cases where certain style choices are quantifiably better than others. In the cases where there isn’t a quantifiable difference, there is still value in making an arbitrary choice one way or the other.
TIP
Style guides provide a common vocabulary so people can concentrate on what you’re saying instead of how you’re saying it.
A good style guide should set out rules for code layout, indentation, whitespace, capitalization, naming, and comments. It is also good to create usage guides that explain best practices and provide guidance on how to use common APIs. Importantly, these guides should explain why a rule exists; over time you will want to reevaluate the rules and should avoid them becoming cargo cults.
Style guides should be enforced by a linter and if possible coupled with a formatter to remove the mechanical steps of adhering to the guide. You don’t want to waste cycles correcting style nits in code reviews.
The ultimate goal is to have all code look like it was written by the same person.
TIP
Lesson 5: Consistency is king.
Evolution of Code
When I was first working on Google Closure there was no simple utility for making XMLHttpRequest
s; everything was rolled up in large, application-specific request utilities.
So, in my naiveté XhrLite was born.
XhrLite became popular—no one wants to use a “heavy” implementation—but its users kept finding features that were missing. Over time small patches were submitted, and XhrLite accumulated support for form encoded data, JSON decoding, XSSI handling, headers, and more—even fixes for obscure bugs in FF3.5 web workers.
Needless to say, the irony of “XhrLite” becoming a distinctly heavy behemoth was not lost, and eventually it was renamed “XhrIo.” The API, however, remained bloated and cumbersome.
TIP
Small changes—reasonable in isolation—evolve into a system that no one would ever design if given a blank canvas.
Evolutionary complexity is almost a force of nature in software development, but it has always seemed more pronounced with JavaScript. One of the strengths that helped spur JavaScript’s popularity is that you can get up and running quickly. Whether you’re creating a simple web app or a Node.js server, a minimal dev environment and a few lines of code yields something functional. This is great when you’re learning, or prototyping, but can lead to fragile foundations for a growing team.
You start out with some simple HTML and CSS. Perhaps you add some event
handlers using jQuery. You add some XHRs, maybe you even start to use
pushState
. Before long you have an actual single-page application,
something you never intended at first. Performance starts to suffer,
there are weird race conditions, your code is littered with setTimeout
s,
there are hard-to-track-down memory leaks…you start wondering if a
traditional web page would be better. You have the duck-billed platypus of
applications.
TIP
Lesson 6: Lay good foundations. Be mindful of evolutionary complexity.
Conclusion
JavaScript’s beauty is in its pervasiveness, its flexibility, and its accessibility. But beauty is also contextual. What started as a “scripting language” is now used by hundred-plus-person teams and forms the building blocks of billion-dollar products. In such situations you can’t write code in the same way you would hacking up a one-person website. So…
-
Code for one another, and use tools to perform mechanical optimizations.
-
Keep it simple; compactness != succinctness.
-
Just because you can doesn’t mean you should.
-
Utilize familiar paradigms and patterns.
-
Consistency is king.
-
Lay good foundations. Be mindful of evolutionary complexity.
Get Beautiful JavaScript now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.