CSE 154 Code Quality Guide(JavaScript) {
A mostly reasonable approach to JavaScript adapted from Airbnb’s JavaScript Style Guide for CSE 154 at the University of Washington.
Questions about the changes? Most of them were to meet the learning objectives of the course and simplify the guide for our needs rather than opinions on code style, but feel free to reach out to the course staff with any questions.
NOTE: All JavaScript Code should lint. Check to see if your code lints by looking for a green check mark in your repo. The linter will automatically run and email you a report if your code does not lint.
Table of Contents
- Variables
- Strings
- Functions
- Naming Conventions
- Comparison Operators & Equality
- Loops
- If/Else Statements
- Boolean Zen
- Curly Braces
- Semicolons
- Comments
- Whitespace & Indentation
- Module Pattern & Strict Mode
- Good JavaScript Design
Variables
-
1.1 Use
let
for all of your variables. Never usevar
.Why?
let
is block-scoped rather than function-scoped likevar
.// bad var a = 1; var b = 2; // good let a = 1; let b = 2;
-
1.2 Use
const
for all of your module-global constants withUPPERCASE_NAMING
. Use this for any “magic” values appearing in your code and declare them at the top of the module pattern (at the module-global level). In general, you should avoid having “random” numbers and strings appear in your code, especially when there is a clear name you could give them. If you instantiateconst
s within functions, use the standard camel case naming convention you would for other variables.Why? Not only does
const
prevent a variable from being reassigned, but constants can also make your code much easier to read. As a note here, it is very common in industry to always prefer const over let when possible. There are many benefits to this approach, but we are going to stick to only using const at the module-global level in this class.// bad const alphabetLength = 26; let ALPHABET_LENGTH = 26; const FAVORITEWEBSITE = 'https://www.omfgdogs.com/'; // good const ALPHABET_LENGTH = 26; const FAVORITE_WEBSITE = 'https://www.omfgdogs.com/';
// bad (function() { function foo() { const SOME_GREETINGS = ['hello', 'hi', 'hey there']; for (let i = 0; i < 12; i++) { // ... } } })(); // good (function() { const MONTHS = 12; function foo() { const someGreetings = ['hello', 'hi', 'hey there']; for (let i = 0; i < MONTHS; i++) { // ... } } })();
NOTE: Both
let
andconst
are block-scoped, meaning that the variables only exist within the nearest curly braces.// const and let only exist in the blocks they are defined in. function foo() { let a = 1; const b = 1; if (a == b) { let c = 1; } console.log(c); // ReferenceError } console.log(a); // ReferenceError console.log(b); // ReferenceError
-
1.3 Use one
let
orconst
declaration per variable or assignment.Why? It’s easier to add new variable declarations this way, and you never have to worry about swapping out a
;
for a,
. You can also step through each declaration with the debugger, instead of jumping through all of them at once.// bad let items = getItems(), goSportsTeam = true, dragonball = 'z'; // good let items = getItems(); let goSportsTeam = true; let dragonball = 'z';
-
1.4 Save expensive function calls into variables, but only once they are needed.
// bad if (list.indexOf('abc') >= 0) { list.remove(list.indexOf('abc')); } // good let index = list.indexOf('abc'); if (index >= 0) { list.remove(index); }
// bad - unnecessary function call function checkName(hasName) { let name = getName(); if (hasName) { // Never used the name variable here! return false; } if (name === 'test') { setName(''); return false; } return name; } // good function checkName(hasName) { if (hasName) { return false; } let name = getName(); if (name === 'test') { setName(''); return false; } return name; }
-
1.5 In general, do not store DOM elements as module-global variables. Instead prefer accessing the DOM.
Why? As a general good coding practice, we want to minimize the number of module-global variables that we use. Since we always have access to the DOM (and DOM lookups are fairly fast), there is usually no need to store these elements or their contents as persisting variables. That said, there are occasional exceptions to this rule, such as when the element is needed by multiple functions extremely frequently but for this class it should be avoided altogether.
Strings
-
2.1 You can use either single or double quotes for strings, just be consistent across all of your code. Don’t use back ticks unless for proper use of template literals.
// bad let name = "human"; let friend = 'dog'; // bad - don't use template literals unless for interpolation let name = `human`; // good let name = 'human'; let friend = 'dog'; // good let name = "human"; let friend = "dog";
Functions
-
3.1 If you have a single function that is very long, break it apart into smaller sub-functions. The definition of “very long” is vague, but often a function longer than 20-30 lines is pushing it.
TIP: If you try to describe the function’s purpose and find yourself using the word “and” a lot, that probably means the function does too many things and should be split into sub-functions.
-
3.2 Always prefer named functions over anonymous functions. A good rule of thumb is that if you can think of a reasonable name, it should be named. An anonymous functions should be 3 or fewer lines of code.
// really bad - long anonymous function fetch(url) .then(statusCheck) .then(res => res.json()) .then(function(json) { names = json.names; for (let i = 0; i < names.length; i++) { sayHello(names[i]); sayGoodbye(names[i]); } }) .catch(console.log);
// also really bad - nested anonymous functions myButton.addEventListener('click', function() { setTimeout(function() { alert('hi!'); }, 1000); }); // good myButton.addEventListener('click', myButtonClick); function myButtonClick() { setTimeout(sayHi, 1000); } function sayHi() { alert('hi!'); }
NOTE: This does not mean that there is no place for anonymous functions and arrow functions. For example, when passing simple callback functions as parameters, it can often be clear and concise to use these. Generally arrow functions are preferred, but either is fine in this course.
// good let arr = [1, 2, 3, 4, 5]; arr = arr.filter(num => num % 2 !== 0); // [1, 3, 5]
-
3.3 Always put default parameters last.
// bad function handleThings(opts = {}, name) { // ... } // good function handleThings(name, opts = {}) { // ... }
Naming Conventions
-
4.1 Avoid single letter names unless it is for an index in a loop. Be descriptive with your naming. Optimally, a reader should understand what your functions and variables do without even reading your comments or the details of your code!
// bad function p() { // ... } // good function getPowerLevel() { // ... }
-
4.2 Use
camelCase
when naming variables, objects and functions. As stated above, useUPPERCASE_NAMING
for constants at the module-global level.// bad let this_is_my_object = {}; function MyFunction() { // ... } // good let thisIsMyObject = {}; function myFunction() { // ... }
Comparison Operators & Equality
-
5.1 Use
===
and!==
over==
and!=
. There is one exception here:myVar != null
can be used to check ifmyVar
has been set. This is a common way to check thatmyVar
is notnull
orundefined
.What’s the difference?
===
performs a “strict” equality check, meaning that it checks value and type.==
only checks for value. For example, the string"0" == 0
istrue
, but"0" === 0
isfalse
. Only using===
is generally good, because it can prevent unwanted bugs, such as any false values being evaluated as equal to null. For reference, here is a great table explaining the difference.// bad if (a == b) { // ... } // good if (a === b) { // ... }
// good if (myVar !== null) { // ... } // good if (myVar !== undefined) { // ... } // also okay for checking that myVar is not null or undefined if (myVar != null) { // ... }
-
5.2 Use shortcuts for booleans, but explicit comparisons for strings and numbers. Shortcuts and explicit comparisons for checking against
null
andundefined
are okay, but be consistent.NOTE: You will often hear instructors refer to this as boolean zen along with this section.
// bad - if isValid is a boolean we can just check if (isValid) // Note: There is some nuance here and occasionally edge cases where this // is necessary. Feel free to ask us if you think you have one of those cases. if (isValid === true) { // ... } // good // Note: This syntax can also be used for checking if a variable // is not null/undefined, but be careful with edge cases described // in the note below. if (isValid) { // ... }
// bad - don't use "hacks" to check if strings are empty let name = ''; ... if (name) { // ... } // good let name = ''; ... if (name !== '') { // checking the length > 0 would also work here // ... }
// bad - don't use "hacks" to check if numbers are 0 if (collection.length) { // ... } // good if (collection.length !== 0) { // ... }
NOTE: Conditional statements such as the
if
statement evaluate their expression using coercion with theToBoolean
abstract method and always follow these simple rules. However, you should avoid using most of these “tricks” as they can make your code very hard to read:- Objects evaluate to true
- Undefined evaluates to false
- Null evaluates to false
- Booleans evaluate to the value of the boolean
- Numbers evaluate to false if +0, -0, or NaN, otherwise true
- Strings evaluate to false if an empty string
''
, otherwise true
More on this here.
if ([0] && []) { // true - an array (even an empty one) is an object, objects will evaluate to true }
-
5.3 Ternaries can be used, but they should not be nested and should be single line expressions. Additionally, you should not use them in cases where they are completely unnecessary.
What’s this? The ternary operator is a common shorthand notation in programming languages in the form of “let variable = expression ? value1 : value2”. If expression is true, the variable gets set to value1 after the “?”. Otherwise, it is set to value2 after the “:”.
// bad let foo = maybe1 > maybe2 ? 'bar' : value1 > value2 ? 'baz' : null; // good - split into 2 separated ternary expressions let maybeNull = value1 > value2 ? 'baz' : null; let foo = maybe1 > maybe2 ? 'bar' : maybeNull;
// bad let foo = a ? a : b; let baz = c ? false : true; // good let foo = a || b; let baz = !c;
-
5.4 When mixing operators, enclose them in parentheses. The only exception is the standard arithmetic operators (
+
,-
,*
, &/
) since their precedence is broadly understood.Why? This improves readability and clarifies the developer’s intention.
// bad let foo = a && b < 0 || c > 0 || d + 1 === 0; // bad let bar = a ** b - 5 % d; // bad // one may be confused into thinking (a || b) && c if (a || b && c) { return d; } // good let foo = (a && b < 0) || c > 0 || (d + 1 === 0); // good let bar = (a ** b) - (5 % d); // good if (a || (b && c)) { return d; } // good let bar = a + b / c * d;
Loops
-
6.1 Use a
for
loop when the number of repetitions is known (definite). Use awhile
loop when the number of repetitions is unknown (indefinite).// bad let i = 0; while (i < arr.length) { console.log(arr[i]); i++; } // good for (let i = 0; i < arr.length; i++) { console.log(arr[i]); }
// bad - notice the middle condition does not use i at all, and it is not used in the loop! let sum = 0; for (let i = 0; sum < 1000; i++) { sum += Math.random(); } // good let sum = 0; while (sum < 1000) { sum += Math.random(); }
-
6.2 Never use break, continue, or empty return statements in this class.
Why? For the length and complexity of the programs we ask you to write, these statements deviate from clear logical flow and you should instead consider ways to refactor your conditional statements and loops to handle different cases without forced “shortcuts”.
// bad - contrived example, but the idea is you can usually replace the break with conditions. while (i < arr.length) { if (i > 5) { break; } console.log(arr[i]); i++; } // good while (i < arr.length && i <= 5) { console.log(arr[i]); i++; }
// bad function foo(num) { if (num === 1) { return; } console.log(num + 5); } // good function foo(num) { if (num !== 1) { console.log(num + 5); } }
If/Else Statements
-
7.1 When using
if/else
statements, properly choose between variousif
andelse
patterns depending on whether the conditions relate to one another. Avoid redundant or unnecessaryif
tests.// bad if (grade >= 90) { console.log('You got an A!'); } if (grade >= 80 && grade < 90) { console.log('You got a B!'); } if (grade >= 70 && grade < 80) { console.log('You got a C!'); } // good if (grade >= 90) { console.log('You got an A!'); } else if (grade >= 80) { console.log('You got a B!'); } else if (grade >= 70) { console.log('You got a C!'); }
-
7.2 Move common code out of
if/else
statements to avoid redundancy.// bad if (x < y) { foo(); x++; console.log('hi'); } else { foo(); y++; console.log('hi'); } // good foo(); if (x < y) { x++; } else { y++; } console.log('hi');
// bad if (foo() || a) { // ... } else if (foo() || b) { // ... } // good let c = foo(); if (c || a) { // ... } else if (c || b) { // ... }
-
7.3 Put
else
on the same line as yourif
block’s closing brace.// bad if (test) { thing1(); thing2(); } else { thing3(); } // good if (test) { thing1(); thing2(); } else { thing3(); }
-
7.4 In case your
if
gets too long or exceeds the maximum line length, each (grouped) condition could be put into a new line. The logical operator should remain at the end of the line. The same is true forelse if
andwhile
.// bad if ((foo === 123 || bar === 'abc') && doesItLookGoodWhenItBecomesThatLong() && isThisReallyHappening()) { // ... } // bad if (foo === 123 && bar === 'abc') { // ... } // bad if ( foo === 123 && bar === 'abc' ) { // ... } // bad if ( foo === 123 && bar === 'abc' ) { // ... } // good if ((foo === 123 || bar === 'abc') && doesItLookGoodWhenItBecomesThatLong() && isThisReallyHappening()) { // ... } // good if (foo === 123 && bar === 'abc') { // ... }
Boolean Zen
-
8.1 Never test if a
boolean
value istrue
orfalse
explicitly.// bad if (myBool === true) { // ... } // good if (myBool) { // ... }
// bad if (myBool === false) { // ... } // good if (!myBool) { // ... }
-
8.2 If you have an
if/else
statement that returns a boolean value based on a test, just directly return the test’s result instead.// bad if (score1 === score2) { return true; } else { return false; } // good return score1 === score2;
Curly Braces
-
9.1 Use braces for the start of any block, regardless of the number of lines. Always go to a new line after the curly braces.
// bad if (test) return false; // bad if (test) return false; // good if (test) { return false; }
// bad function foo() { return false; } // good function foo() { return false; }
Semicolons
-
10.1 Use them.
Why? When JavaScript encounters a line break without a semicolon, it uses a set of rules called Automatic Semicolon Insertion to determine whether or not it should regard that line break as the end of a statement, and (as the name implies) place a semicolon into your code before the line break if it thinks so. ASI contains a few eccentric behaviors, though, and your code will break if JavaScript misinterprets your line break. These rules will become more complicated as new features become a part of JavaScript. Explicitly terminating your statements and configuring your linter to catch missing semicolons will help prevent you from encountering issues.
// bad - raises exception let luke = {} let yoda = {} [luke, yoda].forEach(jedi => jedi.force = 'strong') // good const luke = {}; const yoda = {}; [luke, yoda].forEach(jedi => jedi.force = 'strong');
// bad - returns `undefined` instead of the value on the next line // although you shouldn't return on a new line anyways function foo() { return 'search your feelings, you know it to be foo' } // good function foo() { return 'search your feelings, you know it to be foo'; }
Comments
-
11.1 For JavaScript functions, we ask that you use JSDoc commenting syntax. This syntax provides a clear template for declaring parameters, return types, and special cases. If you have used JavaDoc before, this is a similar commenting style, only for JavaScript. In this class, we expect you to use the
@param
and@return
annotation tags in JSDoc when appropriate for the function. The@param
annotation specifies the name and type of each parameter, as well as what the purpose of that parameter is in the function. The@return
annotation specifies the type and expected value of what is returned given the parameters and any other conditions of the function. You do not need to use any other JSDoc annotations in CSE 154. Here is an example of a function comment skeleton as reference:NOTE: Notice that JSDoc comments start with
/**
not/*
NOTE: The description of your function should describe what the function does, not how it does it. The most important thing is that your comments should describe how the state of the page (and potentially module-global variables) will change by calling the function. Think about how to explain the purpose of the function without implementation details. A good rule of thumb is to never mention processes such as “looping over” things.
NOTE: If there are no parameters or no return value, there should still be a descriptive comment, but you can omit
@param
and@return
annotations.// Single-line JSDoc comment: /** Your comment here - description of function */ function simpleFunction() { } // Multi-line JSDoc comment: /** * brief description of the function * @param {datatype} parameterName1 - parameter description * @param {datatype} parameterName2 - parameter description * @return {datatype} Description of the return value */ function functionName() { }
-
11.2 Use
/* ... */
for multi-line comments.// bad // this comment is getting really really really long // so I am going to break it into multiple lines, but now // there are lots of those ugly start of comment // characters // good /* * This multiline comment is also getting really really long * but I chose to use the correct operator and it is a bit * nicer to look at */
-
11.3 Use
//
for single line comments. When inside of a function, place single line comments on a newline above the subject of the comment, and put an empty line before the comment unless it’s on the first line of a block.NOTE: While not required, many programmers like to comment each module-global variable with a single line comment. These comments can go on the same line as the variable declaration.
// good (function { let active = true; // is current tab })(); // also good (function { // is current tab let active = true; })();
// bad console.log('fetching type...'); // set the default type to 'no type' let type = this.type || 'no type'; // bad function getType() { // set the default type to 'no type' return this.type || 'no type'; } // good function getType() { console.log('fetching type...'); // set the default type to 'no type' return this.type || 'no type'; } // also good function getType() { // set the default type to 'no type' return this.type || 'no type'; }
-
11.4 Start all comments with a space to make it easier to read.
// bad //is current tab let active = true; // good // is current tab let active = true;
Whitespace & Indentation
-
12.1 Place 1 space before the leading brace.
// bad function test(){ console.log('test'); } // good function test() { console.log('test'); } // bad if (myBoolean){ } // good if (myBoolean) { }
-
12.2 Place 1 space before the opening parenthesis in control statements (
if
,while
etc.). Place no space between the argument list and the function name in function calls and declarations.// bad if(isJedi) { fight (); } // good if (isJedi) { fight(); } // bad function fight () { console.log ('Swooosh!'); } // good function fight() { console.log('Swooosh!'); }
-
12.3 Set off operators with spaces.
// bad let x=y+5; // bad for (let i=0;i<arr.length;i++) { // ... } // good let x = y + 5; // good for (let i = 0; i < arr.length; i++) { // ... }
-
12.4 Do not pad your blocks with blank lines. Too much spacing can be just as bad as not enough.
// bad function bar() { console.log(foo); } // bad if (baz) { console.log(qux); } else { console.log(foo); } // good function bar() { console.log(foo); } // good if (baz) { console.log(qux); } else { console.log(foo); }
-
12.5 Avoid spaces before commas and require a space after commas.
// bad let arr = [1 , 2]; // bad let arr = [1,2]; // good let arr = [1, 2];
-
12.6 Always indent one time for each nested block.
// bad if (myBool) { return true; } // good if (myBool) { return true; }
// bad if (myBool) { for (let i = 0; i <arr.length; i++) { doSomething(arr[i]); } doSomethingElse(); return true; } // good if (myBool) { for (let i = 0; i <arr.length; i++) { doSomething(arr[i]); } doSomethingElse(); return true; }
Module Pattern & Strict Mode
-
13.1 Always use the module pattern to contain your code. No code should exist outside of this pattern.
Why? Any code outside of the module pattern becomes global to your entire site. This means that any code you define will be able to interact with other global code and vice versa. The problem here, is that this can create unexpected behavior. For example, if two files both define functions with the same name, the second file’s function would override the first one’s (since HTML is loaded from top to bottom).
See example in 14.2
-
13.2 Always write a
"use strict";
declaration at the top of your file, just above the module pattern to tell the browser to enable strict syntax checking of your JavaScript code.// good "use strict"; (function() { // ... })();
Good JavaScript Design
-
14.1 Never use element.innerHTML for anything other than clearing containers. Prefer createElement().
// bad div.innerHTML = '<img src="dog.jpg" alt="boundless pupper" />'; // good let img = document.createElement('img'); img.src = 'dog.jpg'; img.alt = 'boundless pupper'; div.appendChild(img); // also good - clearing container div.innerHTML = '';
-
14.2 Minimize redundant code as much as possible.
TIP: If the same or extremely similar chunks of code (2+ lines) are repeated, you should probably factor it out. If code is slightly different, try representing the differences as parameters to functions!
// bad foo(); x = 10; y++; // somewhere later in the code... foo(); x = 15; y++;
// good function helper(newX) { foo(); x = newX; y++; } helper(10); // somewhere later in the code... helper(15);
-
14.3 Do not include any CSS styles in JavaScript unless absolutely necessary. Prefer adding and removing classes.
NOTE: There are two primary ways to add a class with JavaScript, using
element.classList.add("class-name")
andelement.className = "class-name"
. However there is a key difference, settingclassName
directly results in deleting all other classes, whileclassList.add()
simply adds a new class. For this reason, we usually prefer utilizing theadd()
andremove()
functions of the classList.TIP: A good rule of thumb is that if you can make a CSS class to easily achieve your desired output, you should do that instead. Styling in JavaScript should only occur when adding dynamic styles (such as a random color) that cannot be predetermined with CSS classes.
// bad div.style.color = 'red'; // better - this isn't great, because it would remove all other classes from div. div.className = 'red'; // good div.classList.add('red');
// good - there is no way to do this in CSS! div.style.color = genRandomColor(); /** * Don't worry about what this function does. It is mostly black magic. * From here: https://www.paulirish.com/2009/random-hex-color-code-snippets/ * @return {String} Random hex code starting with a '#' */ function genRandomColor() { return '#'+Math.floor(Math.random()*16777215).toString(16); }
- 14.4 Never turn in code with
console.log()
,console.error()
,alert()
,debugger
, commented out code, or other debugging code left in. The one exception here is you can use.catch(console.error)
when we do not specify anything else to do with the error.
-
14.5 Always generate valid HTML following the HTML Code Quality Guide
Why? The browser will interact with any code we generate in the same way as it does with code in an HTML file, so we need to follow the same standards. For example, we still need to add alt attributes to images to help screen readers understand them.
// bad let img = document.createElement("IMG"); img.src = "boundelesspupper.png"; document.getElementById("pups").appendChild(img); // good let img = document.createElement("img"); img.src = "boundelesspupper.png"; img.alt = "boundless pupper"; document.getElementById("pups").appendChild(img);