A conundrum: I've been asked to write a JavaScript version of the PEP-8, but I readily accept that JavaScript is not a language that lends itself well to "There Is One Way To Do It"-style guides. I would hate to preach a style that -- in some cases -- is not orthodox, without an explanation of why I like to write (and read) JavaScript this way.
So, by way of needing some semblance of justification for my opinions, I've created a sort of rubric for the goals of any JavaScript style discussion. Note that these rules apply equally to many different styles of writing JavaScript -- as aforementioned, there's no one "blessed" way to write it -- but in presenting my style guide I'll reference these three rules whenever I make a pronouncement. Or, if the reasoning for a given rule lays outside of this rubric (i.e., a rule apropos of nothing!) I'll be sure to note that, as well.
Without further ado, the three laws of JavaScript style guides:
- Portable
- Humane, where it doesn't interfere with being portable
- Efficient, where it doesn't interfere with being portable or humane
This is the primary law. JavaScript must target multiple runtimes by default; perhaps it's best to put this as: "There is no one JavaScript to rule them all." Thus, it's important that code targets the least common denominator of the available environments: your code should run anywhere that has implemented JavaScript -- the language -- to spec.
Note that this does not mean that you should be paranoid.
In practice, this means that one should avoid relying extensively browser-specific APIs (don't write a style guide that says
"use obj.__proto__
to subclass!" -- the reality is that that will never, ever work cross-browser), as well as take care
to ensure that your code does not contain object literals or arrays containing trailing commas. That's it -- that's all. Most of
everything else can be shimmed, so you will be dealing with a modern JavaScript, even in legacy browsers such as IE7.
While this is the paramount law, in practice it's easy enough to avoid violating that you should be spending most of your time thinking of how to fulfill the other two laws. Keep portability in the back of your mind, though, and make sure you test across all of your target environments on a fairly constant basis.
A JavaScript style guide should primarily be judged on the merits of its compatibility with a wide range environments. Dodging environment-specific errors gains that style guide top marks.
Humane implies "easy to read" as well as "easy to write", which in turn implies that your JavaScript should be maintainable. Bonus marks are awarded to any style guide that encourages code that dodges common JavaScript "gotchas", including (but not limited to):
- Preserving
this
inside anonymous functions - Strange closure rules inside of loops
- Coercion (Boxing / Unboxing) problems
Humane JS is JS that avoids, as much as possible, pitfalls in the JavaScript langauge. This is a separate concern from portability
issues in that it targets problems such as scoping rules related to the this
variable, closure behavior in loops, as well as problems with
the complexity resulting from nesting anonymous functions too deeply.
A style guide should encourage behavior that produces maintainable, readable JavaScript that is a pleasure to write.
This is the least of the laws: whenever it does not interfere with being humane or portable, JavaScript should be written in an efficient manner -- this means taking advantage of built-in library functions over (slowly, painfully) reinventing the wheel, or avoiding repeated jQuery lookups. A sane style will encourage authors to avoid expensive operations when possible, and clear paths to take when optimizing existing JavaScript.
- Code Layout
- Semicolons
- Variable Declaration, Object Literals, and Arrays
- Braces and Statements
- Tabs and Indentation
- Variable Naming
- Comments
- Classes and Functions
- An Aside on Classes and Functions
- Behaviors / Idioms
- Avoid "configuration" based APIs
[].slice.call
- Asynchronous Behavior
- Preserving
this
- Prescriptions
- Shorthand Functions
- Simple Is Better Than Complex
- Prefer expressions to statements
- Prefer ES5 Array methods to loop statements
- Prefer operator-based unboxing
- Use Higher-Order Functions to Reduce Repeated Code
- Anonymous Functions and Nesting
- Step 1: The Nest
- Step 2: Pull into function statements
- Step 3: Refactor with an eye towards flattening
- Step 4: Pull common logic out of the function to make it reusable
- In-Browser
- Include ES5-shim
- Event handlers
- Pick a module format
- Separation of concerns in module authoring
Place semicolons before any line beginning with either a [
or (
character. Otherwise, never use semicolons.
a = 3
b = [1,2]
;(x === 1 && console.log(something))
;[1,2].forEach(function() {
})
- HUMANE: reformatting code that includes semicolons often involves a bit of work to get everything correct -- it's too easy to accidentally place a semicolon into the middle of a list of variable declarations and create a bunch of globals.
Commas should be placed one space before each item after the first item (i.e., "comma-first" style). The first item, in the case of object literals, arrays, and expanded function calls, should be indented twice:
var x = 1
, y = 2
, z = {
a: 2
, b: 1
}
, w = 'hello'
, a = [1, 2]
, b = [
1
, 2
, 3
, 4
]
bigfunction(
a
, b
, c
, d
)
Short literals may be included on a single line (but be prepared, almost invariably you'll be expanding them later!)
Prefer to pull var
declaration lists to the first line of their enclosing scope (their current function).
Uninitialized variables should be at the bottom of the list in which they are defined:
// BAD:
var x
, y = 3
// GOOD:
var y = 3
, x
It is acceptable to group variable declarations logically near the beginning of their enclosing scope:
var module1 = require('module1')
, module2 = require('module2')
var value1 = 3
, value2 = 2
- PORTABLE: Comma first makes it much harder to accidentally include trailing commas in literals, which cause horrible errors in Internet Explorer (and in JSON!)
- HUMANE: Makes it easy to spot variables that are "accidentally" in global scope.
- HUMANE: Grouping all of the new variables introduced in a scope at the beginning of that scope makes it easier to determine where
Braces in statements (for
, if
, switch
, and friends) should be placed on the line that necessitates the brace.
else
and else if
statements may be placed between closing and opening braces.
if(x) {
} else if(y) {
} else {
}
switch(something) {
default: break
}
No spaces before the parentheses in statements that require parentheses, one space between the ending parentheses and the beginning brace.
- The JavaScript community has largely standardized on using brace-on-same-line style.
Two spaces for tabs.
There should be one newline separating any top level function from any other top level function, or separating variable definitions from code.
function aaa() {
}
function bbb() {
}
- The JavaScript community has largely standardized around two (2) spaces for tabs.
- HUMANE: The newline separation helps visually separate separate logical bits of code, making the code easier to scan at a glance.
I advocate following Python-style underscored_names
. Avoid creating "private" variables, but if you must, note their private status
with a leading _
. This diverges from the JS Community-at-large, which has standardized around camelCase
naming convention.
a_variable_or_method_name
ClassName
CONSTANT
_private_variable // note that you should not be using these.
- This is largely based on capricious whim.
- I do like that when automatically determining method names (e.g., automatically calling a method when available, similar to Django's "clean_field_name" form methods), a simple
"on_"+name
suffices to determine the new name.- I'm usually working in an environment where Pythonista's will be the primary ones interacting with my JavaScript code, and as such, I like to make them comfortable when they wander into my parlor.
Use //
style comments. Put one space after //
, comments should go on their own line. Avoid JavaDoc style comments. Keep
comments brief, human, and to the point -- explain what you're expecting and returning in the case of a function, or why a
situation is the way it is. Comments should be at the same indentation level as the surrounding code.
// hello world
var a = 1
Warn others with a comment in the following cases:
- A method (a function assigned to a prototype) is being used in such a way that it is not guaranteed to be called on the object it is a member of (unbound function).
- A function is expected to be called in the context of another object (a loose function that expects to be called on another object).
- A function is being rewritten such that it loses its scope resolution chain (e.g., by using
Function.prototype.toString
in conjunction withFunction
).
JavaScript doesn't have classes! Except, in practice, it does. Note that you might get tarred and feathered for referring to the combination of functions and prototypes as a "class" in the community-at-large, but for the purposes of this style guide (and for brevity's sake!) we'll just use the term "class" from here on out.
Use the following pattern to declare a class:
function CamelCase() {
// set sane defaults here.
// do **not** do initialization here.
}
var cons = CamelCase
, proto = cons.prototype
cons.static_method = function() {
}
proto.method = function() {
}
// create a new CamelCase
var dromedary = new CamelCase
This is the "function constructor" pattern. Calling new CamelCase
will return an object who is linked to
the object located at CamelCase.prototype
.
We alias the function itself -- CamelCase
-- to cons
, and CamelCase.prototype
to proto
. This is helpful
as it cuts down on the number of places one has to go to change the class name.
When instantiating a class with
new
, the trailing parens are optional if the class constructor takes no arguments. You should only include the parens in the following situations:
- Your constructor requires arguments.
- You are chaining methods to the
new
expression: e.g.,new Date().getFullYear()
Always use the named function-statement form to declare a class. In modern environments, named functions will contribute their names to the stacktrace when there's an error -- making things more readable -- and in general, seeing this pattern should clue readers in to the fact that you're creating a function constructor, not a simple function.
To inherit from another object, follow this pattern:
function OtherCamelCase() {
// Be sure to call the parent function constructor against `this`
// to ensure any setup performed in the parent constructor is applied
// locally to this new object.
CamelCase.call(this)
}
var cons = OtherCamelCase
, proto = cons.prototype = new CamelCase
proto.constructor = cons
Note that in our constructor, we explictly call the other constructor against the current object. This ensures that any setup that happens in the parent constructor happens to our child object as well.
When aliasing prototype, we further set the prototype to a new instance of CamelCase
. This links the prototype chain
and ensures all methods declared on CamelCase
are available to instances of OtherCamelCase
.
Finally, we reset OtherCamelCase.prototype
's constructor to cons
. <function>.prototype.constructor
is automatically
set on all new functions; however, since we created a new prototype and assigned it to our child class (proto = new CamelCase
),
we inadvertantly lost the constructor
property. proto.constructor = cons
resets it to the appropriate function.
Some notes and preferences:
-
Prefer to only have one class per module: have the export of the module be the class itself.
-
If you are providing a simple "wrapper" function API, be sure to attach your class to that function in some way before exporting it. Future authors can then change the behavior of the class, or create subclasses:
define(function(require) {
var EE = require('eventemitter')
function MyButton(el) {
this.el = el
EE.call(this)
}
var cons = MyButton
, proto = cons.prototype = new EE
proto.constructor = cons
proto.init = function() {
var fn = el.attachEvent || el.addEventListener
fn.apply(el, 'click', this.on_click.bind(this))
}
proto.on_click = function(ev) {
ev.preventDefault()
this.emit('click', this.el)
}
// we are defining this module as a shorthand
// for creating buttons based on element ids.
// NB: we attach the class to the shorthand module
// as cons, and internally refer to the class as
// `button.cons` so that clients may change which
// constructor is being used, or get access to the underlying
// class if need be.
var button = function(str) {
var el = document.getElementById(str)
, btn = new button.cons(el)
btn.init()
return btn
}
button.cons = cons
return btn
})
- If you must have multiple classes in a single file, create appropriately named versions of
cons
andproto
for the subordinate classes (i.e., forMyOtherClass
, useproto_other_cls
andcons_other_cls
). Be sure to attach them to someplace on your primary class so that other authors may modify their behavior.
define(function(require) {
function Child(name) {
this.name = name
}
var cons_child = Child
, proto_child = cons_child.prototype
proto_child.toString = function() {
return "hi, I'm "+this.name
}
function Parent() {
this.children = []
}
var cons = Parent
, proto = cons.prototype
// assign the Child constructor to the Parent prototype and constructor.
// this way authors have access to the original Child class
// via the constructor, and individual `Parent` instances can
// override the type of child class they create without affecting
// all `Parent` instances.
proto.child_class = cons.child_class = cons_child
proto.add_child = function(name) {
// NB: we refer to the instantiation of the `Child` class
// through the `proto.child_class` alias here, using `this`:
var child = new this.child_class(name)
this.children.push(child)
}
return cons
})
- PORTABLE: All browsers support this kind of class creation. ES5-shim does not fully support
Object.create
, so this remains the preferred method of defining classes.- HUMANE: Liberal use of
Function.prototype.bind
, combined with splitting nested functions out into methods, is an easy way of cutting down on nesting.- EFFICIENT: Each method is defined once and only once. Other "class creation" methods, (such as Crockford's module pattern, or function constructors with directly assigned members) must create a new instance of each of their member functions every time a new object is instantiated.
Authors new to JavaScript often trip over how the prototype
member of functions and the new
operator work.
JavaScript is a prototype-based language -- that means that any object (and everything is an object, including functions) has
a secret, private link to a "parent" object. If an object is asked for a property that does not exist on itself,
it delegates its parent object. Eventually, if no object in the chain satisfies that property, the result is returned
as undefined
.
The prototype link is secret; it is invisible to the JS environment in many environments. The officially ordained way
to define a link between a child object and a prototype parent is to use function constructors and the new
keyword.
Here's how that process works:
- In your code, you have:
new Constructor()
- JS looks up the object located at
Constructor.prototype
. - JS takes that object, and creates a new object prototypically linked to it (we'll call him "Gary").
- The
Constructor
function is called, with the new object ("Gary") bound to it asthis
. - If the
Constructor
method returnsundefined
or a primitive value (a string, integer, or boolean), the result of the expression is the new object, "Gary". - If, however,
Constructor
returns an object (anArray
,Object
,Function
, orRegExp
, to name a few), the result of the expression is that object. The new object, "Gary", is automatically garbage collected.
The new
operator creates a new object linked to the provided function's prototype
property, in other words. Hence, to
subclass, we assign our subclass' prototype
property to new Parent
. Then, any object created by new Child
is linked
to Child.prototype
, which is in turn linked to Parent.prototype
(and so on.) By default, all objects are linked to
Object.prototype
.
jQuery plugins often suffer from this malady. If the signature of your API involves passing a nested object structure in with specific keys to control behaviors, consider rewriting it such that each facet of configuration is a different call, or passing in a configuration "instance" that might be reused.
Essentially, when your API takes {objects:{that:{are:{deeply:'nested'}}}}
, you've stated that
you were too lazy to determine, up front, what arguments your API should actually accept; it implies
that clients of your API should expect to constantly refer to documentation (that you must provide)
stating what objects at each level of that configuration instance actually do. In short, this practice
may make writing code easier, but it makes reading or reusing that code exceptionally arduous.
When using Function.prototype.apply
, one may have to coerce the arguments
variable to
an array. Prefer the aforementioned shorthand in all cases.
- HUMANE: Shorter, easier to read. Does not incur a huge performance hit.
When defining an api that has asynchronous behavior, follow these rules:
- The last argument to the function should be a callback that accepts
err
anddata
. - The callback argument should be named
ready
. - Preserve the behavior of the function relative to the stack at all costs. Even if you
can return in the same stack (e.g., you are returning a cache hit), use
setTimeout
to clear the stack before calling the callback with your result. - Avoid
throw
. You may not be throwing an error to any function that can deal with it, on account of having exhausted the prior stack before your function is called. - If you are designing an API that may continuously asynchronously stream data, consider
using an
EventEmitter
over using a callback. - When using
EventEmitter
s to emit events, use the following event names:data
-- Data has been received.error
-- An error was encountered.drain
-- A queue has been emptied, or data has dried up.end
-- The stream has ended, expect no more events.
If, at any point in a function, you need to locally store the value of this
, prefer using
the variable name self
. All subsequent references to this
should use the self
reference
instead (for consistency's sake.) However, if it is at all possible to avoid having to alias
this
, prefer that method, as it will likely lead to flatter, cleaner code.
// BAD:
MyObject.prototype.xhr = function() {
var url = this.endpoint
, xhr = new XMLHttpRequest
xhr.open('GET', url)
var self = this
xhr.onreadystatechange = function() {
if(xhr.readyState === 4) {
self.got_xhr(xhr)
}
}
xhr.send(null)
this.has_sent_xhr()
}
// BETTER:
MyObject.prototype.xhr = function() {
var self = this
, url = self.endpoint
, xhr = new XMLHttpRequest
xhr.open('GET', url)
xhr.onreadystatechange = function() {
if(xhr.readyState === 4) {
self.got_xhr(xhr)
}
}
xhr.send(null)
self.has_sent_xhr()
}
// BEST (avoids need to alias `this`):
MyObject.prototype.xhr = function() {
var url = this.endpoint
, xhr = new XMLHttpRequest
, finished
// bind our `got_xhr` method to `this` and assign it to `finished`.
// this lets us avoid having to alias `this` to `self`.
finished = this.got_xhr.bind(this)
xhr.open('GET', url)
xhr.onreadystatechange = function() {
// we can simply refer to the bound method `finished`,
// and not have to worry at all about the value of `this` in the function.
if(xhr.readyState == 4)
finished(xhr)
}
xhr.send(null)
this.has_sent_xhr()
}
These are less layout-oriented, and more aimed at "you should be doing X while you write JS".
Are acceptable, provided you know what you're doing:
// ACCEPTABLE:
var twos = [[1,2],[1,2],[1,2]].map(function(item) {
return item[1]
})
// BETTER:
var twos = [[1,2],[1,2],[1,2]].map(Function('x', 'return x[1]'))
They are primarily okay to use when you know that you will not be adding more functionality to a loop later on down the line.
Prefer shorter code or simpler expressions to more complex expressions.
Treat expressions like contractors -- the less you have to employ to achieve your result, the better.
The more succinct you can be, the less code there is to understand and debug later. Don't use a ternary operator where a simple boolean short-circuit will suffice.
That said, it's not golf -- you won't score points for using less characters than properly expresses your idea -- sometimes you really do need that ternary.
Using expressions frees you to assign the result of an operation to a variable for debugging purposes, and often conveys a more clear intent than a simple statement.
It's not always possible, but you should double check to see if using an expression would make your code more readable.
This falls under the onus of "prefer expressions to statements", with a twist.
Using the ES5 array primitives frees you from:
- Having to use
obj.hasOwnProperty
infor(var i in obj)
loops to ensure locality of the property - Nearly every kind of closure problem relating to
for
loops
Further, when it comes time to refactor and beautify your code, it offers the possibility of hoisting the body of the loop to the bottom of the current scope (or making it generic, and reusing it elsewhere outside of the current scope).
There will be times you can't (or simply do not want to) use ES5 Array builtins -- this is okay. But always make sure
that you've mentally made an attempt to imagine how your code would look using them. When you are forced to use for
loops, be sure to cache the length
of the array at the outset as follows: for(var i = 0, len = arr.length; i < len; ++i)
.
Always make sure that you use for(var i in x)
loops only on objects, and for(var i = 0, len = x.length; i < len; ++i)
loops only on arrays.
var array = [1,2,3]
, obj = {
key0:0
, key1:1
, key2:2
}
// ABSOLUTELY AWFUL:
for(var i in array) {
}
for(var i = 0; i < obj.length; ++i) {
}
$.each(array, function(idx, el) {
})
// BAD:
for(var i = 0; i < array.length; ++i) {
}
for(var i in obj) {
}
// MERELY UNFORTUNATE:
for(var i = 0, len = array.length; i < len; ++i) {
}
for(var i in obj) if(obj.hasOwnProperty(i)) {
}
// BETTER:
// (if you need to break out of the loop early, consider using `array.some` or `array.every`
// instead of `array.forEach`)
array.forEach(function(item, idx, all) {
})
Object.keys(obj).forEach(function(item, idx, all) {
})
// SLIGHTLY BETTER THAN THE LAST ONE, FOR SMALL ARRAYS:
// (expressions are better than statements)
array.map(function(item, idx, all) {
return value
})
This takes practice, but in general one should prefer numerical operators to unbox objects using ~~
or +
as opposed
to the somewhat bulkier parseInt
or parseFloat
.
- Use
~~
when you absolutely want an integer result. - Use
~~
when you want to round a float to an integer. - Use
+
when you want a float, integer, or NaN result. - Coerce a
Date
to a timestamp integer:+new Date
, or~~new Date
- Use
!~haystack.indexOf(needle)
to signal that you want to check thatneedle
is not a part ofhaystack
. - Similarly, use
!!~haystack.indexOf(needle)
to signal that you want to check thatneedle
is part ofhaystack
. - Emulate ruby-style
unless
orif
suffixes using boolean short circuiting. Note that in the case of assignment, you may have to surround the assignment in parentheses:check_variable && (set_variable = value)
.
- PORTABLE:
Date.now()
may not be defined on your platform.- HUMANE: Less code is easier code to parse visually.
- HUMANE: Preferring unary operations to binary operations makes code easier to write.
If all (or many) of your event handlers prevent default behavior, consider writing a higher-order function that automatically provides this behavior:
function cancel_default(fn) {
return function(ev) {
ev && ev.preventDefault()
return fn.apply(this, [].slice.call(arguments))
}
}
Make like DDT and ruin some nests.
Nesting is the biggest enemy of readability in JavaScript, largely because it's incredibly fast and easy to write deeply nested code and correspondingly incredibly difficult to divine the state of any given point in your program when revisiting it months later.
I am not saying that you need to stop doing this. I am saying that you simply need to take up the practice of revising as you go. You are not done with your code until you have flattened that nest. Simply put, discipline yourself to start looking for the "connective tissue" of these nests -- the variables vital to keep that particular scope functioning correctly. Once you've identified these variables -- and usually it's only one or two variables out of many, many more variables that you really need -- start using named function statements to pull the nest apart. Some tips:
- Try to only target multiline functions -- one liners, or code that has no side effects -- are generally okay where they are.
- Start at the innermost function.
- At first, simply pull the function expression into a named function statement at the bottom of the scope it was in. This will start to collapse the nest, and you may be able to spot the closed-over variables necessary to your target function more easily.
Let's walk through:
function count_lines_and_log(filename, logfile, ready) {
fs.lstat(file, function(err, stat) {
if(err) return ready(err)
if(stat.isDirectory()) return ready(new Error('it is a directory'))
fs.readFile(file, 'utf8', function(err, data) {
if(err) return ready(err)
data = data.split('\n')
fs.lstat(logfile, function(err, stat) {
if(err) return ready(err)
if(stat.isDirectory()) return ready(new Error('it is a directory'))
fs.writeFile(logfile, 'utf8', filename+': '+data.length, function(err) {
return ready(err, data.length)
})
})
})
})
}
function count_lines_and_log(filename, logfile, ready) {
return fs.lstat(file, stat_filename)
function stat_filename(err, stat) {
if(err) return ready(err)
if(stat.isDirectory()) return ready(new Error('it is a directory'))
return fs.readFile(file, 'utf8', read_filename)
function read_filename(err, data) {
if(err) return ready(err)
data = data.split('\n')
return fs.lstat(logfile, stat_logfile)
function stat_logfile(err, stat) {
if(err) return ready(err)
if(stat.isDirectory()) return ready(new Error('it is a directory'))
return fs.writeFile(logfile, filename+': '+data.length, 'utf8', write_complete)
function write_complete(err) {
return ready(err, data.length)
}
}
}
}
}
Where information is shared between scopes, pull it into the parent
scope -- in this case, the only "shared" scope information is the data
--
specifically, how many lines does the file have?
function count_lines_and_log(filename, logfile, ready) {
var linecount
return fs.lstat(file, stat_filename)
function stat_filename(err, stat) {
if(err) return ready(err)
if(stat.isDirectory()) return ready(new Error('it is a directory'))
return fs.readFile(file, 'utf8', read_filename)
}
function read_filename(err, data) {
if(err) return ready(err)
linecount = data.split('\n').length
return fs.lstat(logfile, stat_logfile)
}
function stat_logfile(err, stat) {
if(err) return ready(err)
if(stat.isDirectory()) return ready(new Error('it is a directory'))
return fs.writeFile(logfile, filename+': '+linecount, 'utf8', write_complete)
}
function write_complete(err) {
return ready(err, data.length)
}
}
Use Function.prototype.bind
liberally to pass state between steps, or refactor common
behavior out of the module. Make sure you've made es5-shim
available! Don't go overboard,
your goal is to expose any behavior that requires specific testing to the scope surrounding
the original function. Any other behavior should be listed after the return
in the original
function as a series of function statements in the order that they will be executed.
function fail_if_err(on_error, fn) {
return function(err) {
if(err)
return on_error(err)
return fn.apply(null, [].slice.call(arguments))
}
}
function fail_if_directory(filename, on_error, fn) {
return fail_if_err(function(stat) {
if(stat.isDirectory())
return on_error(new Error('it is a directory'))
return fn()
})
}
function count_lines_and_log(filename, logfile, ready) {
var fail_dir = fail_if_directory.bind(null, filename, ready)
, fail_err = fail_if_err.bind(null, ready)
, read = fail_on_directory(fs.readFile.bind(fs, 'utf8', fail_on_error(read_filename)))
, write = fail_on_directory(write)
, linecount
return fs.lstat(file, fail_on_directory(read))
function read_filename(data) {
return fs.lstat(logfile, write)
}
function write() {
return fs.writeFile(logfile, [filename, linecount].join(': '), 'utf8', write_complete)
}
function write_complete(err) {
return ready(err, data.length)
}
}
It brings pretty much every environment up to modern standards, and lets you use builtin Array
looping properties.
Prefer using these ES5 builtins over other "shim" libraries like underscore or prototype.
- HUMANE: The ES5 functions are a guaranteed standard. No second guessing between how three or four different libraries implement
forEach
.- EFFICIENT: If your browser already supports these functions, it will use the native built-in versions.
- PORTABLE: Brings all of your target enviroments to a single standard.
Prefer explicit ev.preventDefault
over implicit return false
in event listeners.
Attempt to keep that line towards the top of the function. Separate dom handlers and application logic as much as possible.
// BAD:
var a = $('a')
a.bind('click', function() {
// <application logic>
return false
})
// GOOD:
var a = $('a')
a.click(function(ev) {
ev && ev.preventDefault()
// <application logic>
})
- HUMANE: Explicit is better than implicit when having to revisit old code.
The closer it is in practice to how Node.js' flavor of CommonJS works, the better.
This is a completely separate, and more important, issue than choosing a minification strategy for media.
We picked require.js
. Our modules will look like this:
define(function(require) {
var mod = require('./mod')
function MyClass() {
}
var cons = MyClass
, proto = cons.prototype
proto.my_method = function() {
}
return cons
})
JavaScript has suffered, somewhat, from the prevelance of the DOM and jQuery -- and from relentless performance analyzing. There is a tendency towards "kitchen-sink" design (especially in jQuery plugins) that is born somewhat out of the global state present in browser JS, as well as out of a fear of making too many round-trips to the server to collect various bits of infrastructure.
Let me repeat, for emphasis: minification is a separate problem from designing your application.
Your module system, once it has been selected, either has (or can be made to have) excellent minification properties.
In the meantime, your job, dear developer, is to reduce your module to the most bare minimum it can be. If it cannot be described in a sentence, your module may be too big.
DO:
- Favor composition over inheritance.
- Return function constructors (classes) from your module as the primary export.
- Look at SubStack's collection of Node.js modules.
- Look at them again, and make sure you read his README's. That is the level of documentation and polish you should aim for, module-by-module.
- Build big things out of small, swappable parts.
DO NOT:
- Futz around trying to make variables private. This isn't Java.
- Dynamically require other packages. Attempt to put all
require
statements at top level. - Build giant, monolithic apps that can only be "extended" via copy + paste.