This guide is adapted from the jQuery style guide.
ClassNamesLikeThis
methodNamesLikeThis
variableNamesLikeThis
parameterNamesLikeThis
propertyNamesLikeThis
SYMBOLIC_CONSTANTS_LIKE_THIS
When naming variables and properties referring to jQuery element
objects, prefix the name with $
:
function doSomethingFancy(selector) {
var $elements = $(selector);
...
}
Private methods and properties (in files, classes, and namespaces) should be named with a leading underscore.
While we do not currently use any compilers to enforce this, clients of an API or class are expected to respect these conventions.
function _PrivateClass() {
// should not be instantiated outside of this file
}
function PublicClass(param) {
this.publicMember = param;
this._privateMember = new _PrivateClass();
}
var x = new _PrivateClass(); // OK - we’re in the same file.
var y = new PublicClass(); // OK
var z = y._privateMember; // NOT OK!
Rationale: leading underscores for private methods and properties is consistent with the styles used in numerous JavaScript libraries, many of which we include in our code base (e.g. Backbone). It is also consistent with our Python style guide, lowering the mental effort for developers to switch between the two.
file-names-like-this.js
template-names-like-this.handlebars
Use 4-space indenting for all code. Do not use tabs.
Extra indentation should be used to clearly distinguish multiline conditionals from the following block of code (similar to the PEP8 rule for Python code).
No:
if (someReallyLongBooleanVariableIMeanReallyLong &&
someOtherBoolean) {
return "monkeys";
}
Yes:
if (someReallyLongBooleanVariableIMeanReallyLong &&
someOtherBoolean) {
return "monkeys";
}
Braces should always be used on blocks.
if/else/for/while/try
should always have braces and always go on
multiple lines, with the opening brace on the same line.
No:
if (true)
blah();
Yes:
if (true) {
blah();
}
else/else if/catch
should go on the same line as the brace:
if (blah) {
baz();
} else {
baz2();
}
Lines should not exceed 79 characters. (This is called the "80 character rule," leaving 1 character for the newline.)
This is consistent with our Python style guide, which adheres to PEP8.
Separate first party and third party require()
lines, and sort
require()
lines.
This is to mirror our Python import style, though there are no "system" imports in JavaScript.
"First party" code is anything we wrote whose primary source lives in the repository its being used in. Underscore is third party because we didn't write it. KaTeX is third party in webapp because even though we wrote it, its primary sources lives in a different repository.
Imports should be sorted lexicographically (as per unix sort
).
No:
var _ = require("underscore");
var $ = require("jquery");
var APIActionResults = require("../shared-package/api-action-results.js");
var Cookies = require("../shared-package/cookies.js");
var cookieStoreRenderer = require("../shared-package/cookie-store.handlebars");
var HappySurvey = require("../missions-package/happy-survey.jsx");
var DashboardActions = require('./datastores/dashboard-actions.js');
var React = require("react");
var UserMission = require("../missions-package/user-mission.js");
var Kicksend = require("../../third_party/javascript-khansrc/mailcheck/mailcheck.js");
Yes:
var $ = require("jquery");
var Kicksend = require("../../third_party/javascript-khansrc/mailcheck/mailcheck.js");
var React = require("react");
var _ = require("underscore");
var APIActionResults = require("../shared-package/api-action-results.js");
var Cookies = require("../shared-package/cookies.js");
var DashboardActions = require('./datastores/dashboard-actions.js');
var HappySurvey = require("../missions-package/happy-survey.jsx");
var UserMission = require("../missions-package/user-mission.js");
var cookieStoreRenderer = require("../shared-package/cookie-store.handlebars");
Object destructuring should go after all require lines.
Write requires on a single line, even if they extend past 80 chars, so they are easier to sort. Our linter automatically skips require lines when checking line length.
No:
var React = require("react");
var ReactART = require("react-art");
var Group = ReactART.Group;
var Path = ReactART.Path;
var _ = require("underscore");
var ItemStore = require("./item-store.jsx");
Yes:
var React = require("react");
var ReactART = require("react-art");
var _ = require("underscore");
var ItemStore = require("./item-store.jsx");
var Group = ReactART.Group;
var Path = ReactART.Path;
Inline style comments should be of the //
variety, not the /* */
variety.
All files and classes should have JSDoc comments.
JSDoc can be parsed by a number of open source tools, and must be well-formed.
Syntax:
/**
* A JSDoc comment should begin with a slash and 2 asterisks.
*/
Top-level (top-of-file) comments are designed to orient readers unfamiliar with the code to what is in this file and any other disclaimers clients of the code should be given. It should provide a description of the file's contents and any dependencies or compatibility information. As an example:
/**
* Various components to handle management of lists of coaches for
* the profile page.
*
* These utilities were not written to be a general purpose utility
* for the entire code base, but has been optimized with the
* assumption that the Profile namespace is fully loaded.
*/
Class comments should be used for every class, and give a description along with appropriate type tags (see "Methods and properties" comments for more information on types on the constructor).
/**
* Class making something fun and easy.
*
* @param {string} arg1 An argument that makes this more interesting.
* @param {Array.<number>} arg2 List of numbers to be processed.
*/
function SomeFunClass(arg1, arg2) {
// ...
}
All non-trivial methods and properties should also have JSDoc comments.
Type annotations are strongly encouraged; if there is even a slight chance that the type will be ambiguous to future readers, put in a type annotation.
Type annotations are based on the ES4/JS2 type system, and are documented in the Google JavaScript style guide.
@param
and @return
type annotations that have comments that do not
fit on one line wrap to the next line and indent 4 spaces.
Example:
/**
* A UI component allows users to select badges from their full list
* of earned badges, displaying them in a container.
* Expects a Badges.BadgeList as a model.
*/
Badges.DisplayCase = Backbone.View.extend({
/**
* Whether or not this is currently in edit mode and the full
* badge list is visible.
*/
editing: false,
/**
* The full user badge list available to pick from when in edit mode.
* @type {Badges.UserBadgeList}
*/
fullBadgeList: null,
/**
* Enters "edit mode" where badges can be added/removed.
* @param {number=} index Optional index of the slot in the display-case
* to be edited. Defaults to the first available slot, or if none
* are available, the last used slot.
* @return {Badges.DisplayCase} This same instance so calls can be
* chained.
*/
edit: function(index) {
…
},
...
};
Prefer ===
(strict equality) to ==
due to the numerous oddities
related to JavaScript's type coercion.
The only valid use of ==
is for comparing against null and undefined
at the same time:
// Check null and undefined, but distinguish between other falsey values
if (someVariable == null) {
Though you will often want to just check against falsey values, and
can just say if (!someVariable) { ... }
.
Always use []
and {}
style literals to initialize arrays and
objects, not the Array
and Object
constructors.
Array constructors are error-prone due to their arguments: new Array(3)
yields [undefined, undefined, undefined]
, not [3]
.
To avoid these kinds of weird cases, always use the more readable array literal.
Object constructors don't have the same problems, but follow the same
rule for consistency with arrays. Plus, {}
is more readable.
No:
var a = "foo",
b = a + "bar",
c = fn(a, b);
Yes:
var a = "foo";
var b = a + "bar";
var c = fn(a, b);
A single var statement is bad because:
- If you forget a comma, you just made a global
- It originated when people wanted to save bytes, but we have a minifier
- It makes line-based diffs/editing messier
- It encourages C89-style declarations at the top of scope, preventing you from only declaring vars before first use, the latter preferable as it conveys intended scope to the reader
When you want a link-like thing rather than a button to trigger a JavaScript operation, rather than going to a new address.
Here's a discussion on Stack Overflow about options: http://stackoverflow.com/questions/134845/href-tag-for-javascript-links-or-javascriptvoid0
No:
<a href="#">Flag</a>
Yes:
<a href="javascript:void 0">Flag</a>
In most of our major JavaScript repositories (webapp, perseus, khan-exerises), we use some form of module system like RequireJS or browserify, or in the case of webapp our own home built thing that works similarly to browserify.
In all of these cases, there are mechanisms for an explicit import/export mechanism rather than using global variables to export functionality.
No:
window.Jungle = {
welcome: function() {
// ...
},
haveFever: function() {
// ...
}
};
NO:
window.welcome = function() {
// ...
};
window.haveFever = function() {
// ...
};
Yes:
var Jungle = {
welcome: function() {
// ...
},
haveFever: function() {
// ...
}
};
module.exports = Jungle;
You can export multiple objects in one file, but consider if it wouldn't be better to split up the file to maintain one export per file.
Several of our supported browsers support only ES5 natively. We use polyfills to emulate some -- but not all -- ES6 and ES7 language features so they run on ES5-capable browsers.
In some cases, we do not yet allow a new language feature, if it's expensive to polyfill. In others, we require using the newer language feature and avoiding the old:
Construct | Use... | ...instead of |
---|---|---|
backticks | `http://${host}/${path}` |
"http://" + host + "/" + path |
destructuring | var { x, y } = a; |
var x = a.x; var y = a.y; |
fat arrow | foo(() => { ... }) |
foo(function() { ... }.bind(this)) |
let/const | let a = 1; const b = "4EVAH"; a++; |
var a = 1; var b = "4EVAH"; a++; |
includes | array.includes(item) |
array.indexOf(item) !== -1 |
for/of | for (const [key, value] of Object.entries(obj)) { ... } |
_.each(obj, function(value, key) { ... }) |
spread | { ...a, ...b, c: d } |
_.extend({}, a, b, { c: d }) |
rest params | function(bar, ...args) { foo(...args); } |
function(bar) { var args = Array.prototype.slice.call(arguments, 1); foo.apply(null, args); } |
Arrow functions are easier to read (and with Babel, more efficient)
than calling bind
manually.
The magic arguments
variable has some odd quirks. It's simpler to
use rest params like (...args) => foo(args)
.
+
is not forbidden, but backticks are encouraged!
Continue to use React's createClass
, which works with React mixins.
For classes outside of React -- which should actually be pretty rare -- there is no style rule whether to use ES6 classes or not.
This rule may change once React supports mixins with ES6 classes.
This is because the polyfill for these constructs generates very large code.
This rule may change once all our supported browsers support ES6 natively.
The polyfills for these, though not huge, are large enough it's not
worth the (small) benefit of using these classes for hashtables
instead of just using object
.
This rule may change if strong enough support for these types is evinced.
let
is superior to var
, so prefer it for new code.
This rule will become mandatory everywhere once we have done a fixit
to replace all uses of var
in existing files.
We use $
as the jQuery identifier, as opposed to typing out jQuery
in full.
No:
jQuery(".some-class span").hide();
Yes:
$(".some-class span").hide();