Skip to content

Instantly share code, notes, and snippets.

@chrisdickinson
Last active December 26, 2015 15:45
Show Gist options
  • Star 5 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save chrisdickinson/243ce66e936d95fab40b to your computer and use it in GitHub Desktop.
Save chrisdickinson/243ce66e936d95fab40b to your computer and use it in GitHub Desktop.

Three Laws of JavaScript

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:

  1. Portable
  2. Humane, where it doesn't interfere with being portable
  3. Efficient, where it doesn't interfere with being portable or humane

Portable

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

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.

Efficient

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.

De Stijl

  • 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

Code Layout

Semicolons

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.

Variable declaration, Object Literals, and Arrays

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 and statements

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.

Tabs and indentation

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.

Variable Naming

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.

Comments

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 with Function).

Classes and Functions

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.

okay

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.

Nota Bene:

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 and proto for the subordinate classes (i.e., for MyOtherClass, use proto_other_cls and cons_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.

An Aside on Classes and Functions

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 as this.
  • If the Constructor method returns undefined 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 (an Array, Object, Function, or RegExp, 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.


Behaviors / Idioms

Avoid "configuration"-based APIs.

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.

Prefer the shorter [].slice.call to the more verbose Array.prototype.slice.call

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.

Asynchronous Behavior

When defining an api that has asynchronous behavior, follow these rules:

  • The last argument to the function should be a callback that accepts err and data.
  • 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 EventEmitters 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.

Preserving this

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()
    }

Prescriptions

These are less layout-oriented, and more aimed at "you should be doing X while you write JS".

Shorthand Functions

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.

Simple Is Better Than Complex

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.

Prefer expressions to statements

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.

Prefer ES5 Array methods (and Object.keys) to language-level for constructions

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 in for(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
    })

Prefer operator-based unboxing

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 that needle is not a part of haystack.
  • Similarly, use !!~haystack.indexOf(needle) to signal that you want to check that needle is part of haystack.
  • Emulate ruby-style unless or if 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.

Use higher-order functions to reduce repeated code.

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))
  }
}

Anonymous Functions and Nesting

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:

Step 1: The Nest
    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)
            })
          })  
        })
      })
    }
Step 2: Pull into function statements
    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)
            }
          }
        }
      }
    }
Step 3: Refactor with an eye towards flattening

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)
      }
    }
Step 4: Pull common logic out of the function to make it reusable

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)
      }
    }

In-Browser

Include ES5-shim.

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.

Event Handlers

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.

Pick a module format.

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
    })

Your module should be smaller than that.

No really, it should still be smaller.

I'm not even kidding, make it as small as you possibly can.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment