Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
1573 lines (1171 sloc) 47.1 KB

CTU JavaScript Guide

This guide aims to provide the ground rules for an application’s JavaScript code, such that it’s highly readable and consistent across different developers on a team. The focus is put on quality and coherence across the different pieces of your application.

The guide goes a bit beyond code style guidelines; it offers recommendations for technology stack, tooling and programming approach. It is purposefully opinionated – you don’t need to agree with everything, but you should at least think why you disagree.

This guide is aimed at needs of ICT development teams at Faculty of Information Technology of Czech Technical University in Prague, but you are more than welcome to adapt it for your organisation.

tl;dr

Table of Contents

ES6

Learn about ECMAScript 6, its new syntax and features, and use it in your project today.

For browser-oriented projects transpile your project with Babel.

For server oriented projects, you can use most features of ES6 natively in Node.js. Alternatively you can use babel-node from the Babel project. Consult ES6 Compatibility Table and node.green.

You may want to avoid Babel in smaller projects where the code compilation brings unjustified overhead.

See tooling for tips how to manage your build process.

Modules

Organise your code into modules. Use ES6 modules (with Babel) or CommonJS (in other cases). Each module is scoped with explicit exports and does not pollute global namespace. Additionally you can easily share modules between server and browser projects.

For browser projects, you can compose modules into a single bundle with tools like Browserify or Webpack. See tooling for more information.

Module systems also provide us with dependency injection patterns, which are crucial when it comes to testing individual components in isolation.

Strict Mode

Put 'use strict' at the top of your modules. Strict mode allows you to catch nonsensical behaviour, discourages poor practices, and is faster because it allows compilers to make certain assumptions about your code.

Linter can warn you about this. Babel inserts 'use strict' by default, so this rule does not apply.

Linting

Use ESLint for linting your code with our provided rules.

Code Style

We build upon the Standard code style. You may not like some features of it, but your team members may like them – and vice versa. The point is: somebody else made these decisions for you, so you can carry on with your life and don’t discuss the color of the bikeshed.

Indentation

Spacing must be consistent across every file in the project. Put .editorconfig configuration file into your project. These are recommended defaults:

# editorconfig.org
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false

Use 2 spaces for indentation. The .editorconfig file can take care of that for us and everyone will be able to create the correct spacing by pressing the tab key.

Spacing

Spacing doesn’t just entail indentation, but also the spaces before, after, and in between arguments of a function declaration.

Put space before parenthesis and after comma.

function (a, b) {
  // your stuff
}

if (true) {
  // other stuff
} else {
  // more stuff
}

Set off operators with spaces.

// bad
const x=y+5

// good
const x = y + 5

End files with a single newline character (.editorconfig will take care of it for you).

(function(global) {
  // ...stuff...
})(this)↵

When making long method chains, use indentation. Use a leading dot, which emphasises that the line is a method call, not a new statement.

// bad
$('#items').
  find('.selected').
    highlight().
    end().
  find('.open').
    updateCount()

// good
$('#items')
  .find('.selected')
    .highlight()
    .end()
  .find('.open')
    .updateCount()

Linter will remind you about this style and automatic formatter can fix the code for you.

Line Length

Where possible, improve readability by keeping lines below the 100-character mark. Never go over 120 characters per line.

Brace Style

Per Standard, use the “one true brace style.”

Braces are always required for conditionals.

// bad
function foo ()
{
  return true
}

// good
function foo () {
  return true
}
// bad
if (foo) bar()

// good
if (foo) { bar() }
// bad
if (bar)
{
  for (let i; i < 10; i++)
    console.log('Nan')
}

// good
if (bar) {
  for (let i; i < 10; i++) {
    console.log('Nan')
  }
}
// bad
try
{
  somethingRisky()
} catch (e)
{
  handleError()
}

// good
try {
  somethingRisky()
} catch (e) {
  handleError()
}
// bad
if (foo) {
  bar()
}
else {
  baz()
}

// good
if (foo) {
  bar()
} else {
  baz()
}

Semicolons;

We don’t use semicolons; we rely on Automatic Semicolon Insertion (ASI) instead. While there are some caveats, you shouldn’t run into them if you are not doing anything crazy in your code (and thus breaking this guide). The most notable caveat is this one:

End of line is not treated as semicolon if the next line starts with [, (, + etc.

The following code won’t work:

// bad
let a = b + c
(d + e).print()

Linter will warn you about these exceptions. You can solve this by putting semicolon at the beginning of the line:

// good
let a = b + c
;(d + e).print()

However, you may want to keep using semicolons in JavaScript if your project includes languages with mandatory semicolons, like Java. Semistandard is then for you.

Regardless of your choice, a linter should be used to catch unnecessary, unintentional or missing semicolons.

Quotes

Use single quote ' for quoting strings consistently throughout your codebase.

// bad
const message = "o hai!"

// good
const message = 'o hai!'

Commas

Do not use leading commas, it’s just plain ugly.

// bad
const story = [
    once
  , upon
  , aTime
]

// good
const story = [
  once,
  upon,
  aTime,
]

Use trailing commas, it makes code diffs cleaner and modifications simpler.

// bad
const hero = {
  firstName: 'Ramona',
  lastName: 'Flowers'
}

// good
const hero = {
  firstName: 'Ramona',
  lastName: 'Flowers',
}
// bad
const heroes = [
  'Batman',
  'Iron Man'
]

// good
const heroes = [
  'Batman',
  'Iron Man',
]

Naming

Variables and functions must have meaningful names so that you don’t have to resort to commenting what a piece of functionality does. Instead, try to be expressive while succinct, and use meaningful variable names.

// bad
function a (x, y, z) {
  return z * y / x
}
a(4, 2, 6) // => 3

// good
function ruleOfThree (had, got, have) {
  return have * got / had
}
ruleOfThree(4, 2, 6) // => 3

Use camelCase when naming objects, functions, and instances.

// bad
const this_is_my_object = {}
function MYFUNCTION () {}

// good
const thisIsMyObject = {}
function myFunction () {}

Use PascalCase when naming constructors (object factories) or classes.

// bad
function user (options) {
  return {
    name: options.name
  }
}
const myUser = user({name: 'Ada'})

// good
function User (options) {
  return {
    name: options.name
  }
}
const myUser = User({name: 'Ada'})

Variables

Declare variables where you need them, but place them in a reasonable place (e.g. not inside conditions).

Keeping variable declarations to one per line is encouraged. Always use one const, let or var statement for each assignment.

// bad
const foo = 1,
      bar = 2

// good
const foo = 1
const bar = 2
// bad
let a
  , b

// good
let a
let b
// bad
const foo = 1

if (foo > 1) {
  let bar = 2
}

// good
const foo = 1
let bar

if (foo > 1) {
  bar = 2
}

Variable declarations that aren’t immediately assigned a value are acceptable to share the same line of code.

Avoid using var. Use const for all of your references. While this won’t prevent mutation of structures (i.e. objects or arrays), it will prevent a reassignment of the variable.

If you must mutate references, use let instead of var. const and let are scoped to block (i.e. within braces {}), while var is scoped within a function. See JavaScript Scoping and Hoisting for more details.

// bad
var a = 1
var b = 1

// good
const a = 1
let b = 1

if (true) {
  b += 1
}

Group declarations by their type: const first, let second.

// bad
const a = 1
let b, c
const d = 2

// good
const a = 1
const d = 2

let b, c

// acceptable
let foo, bar

Strings

Use ES6 template strings for strings formatting.

// bad
const message = 'oh hai ' + name + '!'

// good
const message = `oh hai ${name}!`

Equality

Avoid using == and != operators, always favor === and !==. These operators are called the “strict equality operators,” while their counterparts will attempt to cast the operands into the same value type.

// bad
function isEmptyString (text) {
  return text == ''
}
isEmptyString(0) // => true

// good
function isEmptyString (text) {
  return text === ''
}
isEmptyString(0) // => false

There is one exception: use == and != to check for null or undefined values in one statement.

// bad
if (text === null || text === undefined) {
  console.log('text is null or undefined')
}

// good
if (text == null) {
  console.log('text is null or undefined')
}

“Truthy” and “Falsy” Values

Keep comparisons simple, but make sure you know what you are doing.

// bad
if (name !== '') {
  // ...stuff...
}

// good
if (name) {
  // ...stuff...
}
// bad
if (collection.length > 0) {
  // ...stuff...
}

// good
if (collection.length) {
  // ...stuff...
}

Conditional statements such as the if statement evaluate their expression using and convert value to boolean using these rules:

  • 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
if ([0]) {
  console.log('this is true')
  // An array is an object, objects evaluate to true
}

Ternary Operators

Avoid nested ternary operators. Ternary operators are fine for clear-cut conditionals, but unacceptable for confusing choices.

jQuery is a prime example of a codebase that’s filled with nasty ternary operators.

// bad
function calculate (a, b) {
  return a && b ? 11 : a ? 10 : b ? 1 : 0
}

// good
function getName (mobile) {
  return mobile ? mobile.name : 'Generic Player'
}

In cases that may prove confusing just use if and else statements instead.

Functions

When declaring a function, always use the function declaration form instead of function expressions. Because hoisting.

// bad
const sum = function (x, y) {
  return x + y
}

// good
function sum (x, y) {
  return x + y
}

That being said, there’s nothing wrong with function expressions that are just currying another function.

// good
const plusThree = sum.bind(null, 3)

Keep in mind that function declarations will be hoisted to the top of the scope so it doesn’t matter the order they are declared in. That being said, you should always keep functions at the top level in a scope, and avoid placing them inside conditional statements.

// bad
if (Math.random() > 0.5) {
  sum(1, 3)

  function sum (x, y) {
    return x + y
  }
}

// good
if (Math.random() > 0.5) {
  sum(1, 3)
}

function sum (x, y) {
  return x + y
}

// also good
function sum (x, y) {
  return x + y
}

if (Math.random() > 0.5) {
  sum(1, 3)
}

Anonymous Functions

Use arrow functions for simple anonymous functions.

// bad
[1, 2, 3].map(function (x) {
  return x * x
})

// good
[1, 2, 3].map((x) => {
  return x * x
})

Omit braces, parentheses, and return if the function accepts a single argument and fits into a single line.

// better
[1, 2, 3].map(x => x * x)

Whenever a method is non-trivial, make the effort to use a named function declaration rather than an anonymous function. This will make it easier to pinpoint the root cause of an exception when analyzing stack traces.

// bad
function once (fn) {
  let ran = false
  return function () {
    if (ran) { return }
    ran = true
    fn.apply(this, arguments)
  }
}

// good
function once (fn) {
  let ran = false
  return function run () {
    if (ran) { return }
    ran = true
    fn.apply(this, arguments)
  }
}

Function Parameters

Learn about default, rest, and spread parameters in ES6.

Never name function parameter arguments since it is a reserved variable name (in fact, it using it should cause a syntax error).

// bad
function nope (name, options, arguments) {
  // ...stuff...
}

// good
function yup (name, options, args) {
  // ...stuff...
}

Do not use arguments keyword in function. Use rest arguments instead.

// bad
function concatenateAll () {
  const args = Array.prototype.slice.call(arguments)
  return args.join('')
}

// good
function concatenateAll (...args) {
  return args.join('')
}

Use default parameter syntax rather than mutating function arguments.

// really bad
function handleThings (opts) {
  // No! We shouldn’t mutate function arguments.
  // Double bad: if opts is falsy it’ll be set to an object which may
  // be what you want but it can introduce subtle bugs.
  opts = opts || {}
  // ...
}

// still bad
function handleThings (opts) {
  if (opts === void 0) {
    opts = {}
  }
  // ...
}

// good
function handleThings (opts = {}) {
  // ...
}

Conditional Return

Avoid keeping indentation levels from raising more than necessary by using guard clauses instead of flowing if statements.

// bad
if (car) {
  if (black) {
    if (turbine) {
      return 'batman!'
    }
  }
}

// good
if (!car) { return }
if (!black) { return }
if (!turbine) { return }

return 'batman!'
// bad
if (condition) {
  // 10+ lines of code
}

// good
if (!condition) { return }

// 10+ lines of code

Destructuring

Use object destructuring when accessing and using multiple properties of an object.

// bad
function fullName (user) {
  const firstName = user.firstName
  const lastName = user.lastName

  return `${firstName} ${lastName}`
}

// good
function fullName(obj) {
  const { firstName, lastName } = obj
  return `${firstName} ${lastName}`
}

// best
function fullName ({ firstName, lastName }) {
  return `${firstName} ${lastName}`
}

Use array destructuring.

const arr = [1, 2, 3, 4]

// bad
const first = arr[0]
const second = arr[1]

// good
const [first, second] = arr

Use object destructuring for multiple return values, not array destructuring. If you add properties over time or change the order of things, you won't break call sites.

// bad
function processInput (input) {
  // then a miracle occurs
  return [left, right, top, bottom]
}

// the caller needs to think about the order of return data
const [left, __, top] = processInput(input)

// good
function processInput (input) {
  // then a miracle occurs
  return { left, right, top, bottom }
}

// the caller selects only the data they need
const { left, right } = processInput(input)

Prototypes & Classes

Extending Native Prototypes

Hacking native prototypes should be avoided at all costs. Use a function instead. If you must extend the functionality in a native type, try using something like poser instead.

// bad
String.prototype.half = function () {
  return this.substr(0, this.length / 2)
}

// good
function half (text) {
  return text.substr(0, text.length / 2)
}

Prototypal Inheritance

Avoid prototypal inheritance models unless you have a very good memory-consumption reason to justify yourself.

  • Prototypal inheritance boosts need for this through the roof.
  • It’s way more verbose than using plain objects.
  • It causes headaches when creating new objects.
  • Needs a closure to hide valuable private state of instances.
  • Just use plain objects instead.

Class Based Inheritance

Avoid ES6 classes. Since it is just a syntactic sugar over prototypal inheritance, same caveats apply.

You should always prefer composition over inheritance, but class-based OOP makes inheritance very easy and composition unnecessarily complicated. Class-based OOP has a tendency to invite a lot of additional complexity to manage its own shortcomings.

Simple object factories and stateless, pure functions get you usually further without all the complexity introduced by class-based object-oriented programming.

// bad
class Point {
  constructor (x, y) {
    [this.x, this.y] = [x, y]
  }
  get x () {
    return this.x
  }
  get y () {
    return this.y
  }
}

// good
function Point (x, y) {
  return Object.freeze({x, y})
}

Recommended Reading

Arrays

Instantiate arrays using the square bracketed notation []. If you have to declare a fixed-dimension array for performance reasons then it’s fine to use the new Array(length) notation instead.

Loops

Don’t declare functions inside of loops. Whenever possible, use .forEach instead of a for loop. There is also no issue with declaring anonymous function within other functions.

const values = [1, 2, 3]

// bad
for (let i = 0; i < values.length; i++) {
  setTimeout(function () {
    console.log(values[i])
  }, 1000 * i)
}

// also bad
for (let i = 0; i < values.length; i++) {
  setTimeout((i) => {
    return function () {
      console.log(values[i])
    }
  }(i), 1000 * i)
}

// somewhat acceptable
for (let i = 0; i < values.length; i++) {
  setTimeout(function (i) {
    console.log(values[i])
  }, 1000 * i, i)
}

// a bit more acceptable
for (let i = 0; i < values.length; i++) {
  wait(i)
}

function wait (i) {
  setTimeout(function () {
    console.log(values[i])
  }, 1000 * i)
}

// better
[1, 2, 3].forEach((value, i) => {
  setTimeout(() => {
    console.log(value)
  }, 1000 * i)
})

Manipulation

It’s about time you master array manipulation! Learn about the basics. It’s way easier than you might think.

Learn and abuse the functional collection manipulation methods. These are so worth the trouble.

Copy

Use array spreads ... to copy arrays.

// bad
const len = items.length
const itemsCopy = []

for (let i = 0; i < len; i++) {
  itemsCopy[i] = items[i]
}

// good
const itemsCopy = [...items]

Array-like Objects

Whenever you have to manipulate an array-like object, use Array.from.

const divs = document.querySelectorAll('div')

// bad
const nodes = []
for (let i = 0; i < divs.length; i++) {
  nodes.push(divs[i])
}

// good
const nodes = Array.from(divs)

Objects

Instantiate using the egyptian notation {}.

// bad
const obj = new Object()

// good
const obj = {}

When returning objects, consider using Object.freeze to enforce immutability of the object, or at least Object.seal to maintain object’s properties.

ES6 Shorthands

ES6 has some nice improvements for object literal which reduce boilerplate.

Use object method shorthand:

// bad
const atom = {
  value: 1,

  addValue: function (value) {
    return atom.value + value
  },
}

// good
const atom = {
  value: 1,
  // you can omit function
  addValue(value) {
    return atom.value + value
  },
}

Use property value shorthand.

const lukeSkywalker = 'Luke Skywalker'

// bad
const obj = {
  lukeSkywalker: lukeSkywalker,
}

// good
const obj = {
  lukeSkywalker,
}

console statements

Preferably bake console statements into a service that can easily be disabled in production. Alternatively, don’t ship any console.log printing statements to production distributions.

Comments

Comments aren’t meant to explain what the code does. Good code is supposed to be self-explanatory. If you’re thinking of writing a comment to explain what a piece of code does, chances are you need to change the code itself. The exception to that rule is explaining what a regular expression does. Good comments are supposed to explain why code does something that may not seem to have a clear-cut purpose.

// bad
const p = $('<p/>') // create the centered container
p.center(div)
p.text('foo')

// good
const container = $('<p/>')
const contents = 'foo'

container.center(parent)
container.text(contents)
// good
megaphone.on('data', function (value) {
  // the megaphone periodically emits updates for container
  container.text(value)
})
// good
const numeric = /\d+/ // one or more digits somewhere in the string
if (numeric.test(text)) {
  console.log('so many numbers!')
}

Commenting out entire blocks of code should be avoided entirely, that’s why you have version control systems in place!

Polyfills

Where possible use the native browser implementation and include a polyfill that provides that behaviour for unsupported browsers. This makes the code easier to work with and less involved in hackery to make things just work.

If you can’t patch a piece of functionality with a polyfill, then wrap all uses of the patching code in a globally available method that is accessible from everywhere in the application.

Asynchronous Behaviour

If your application works with external data or handles user interaction, you will have to deal with asynchronous code. Typically we pass around callbacks, for example (in Node):

fs.readdir(source, function(err, files) {
  if (err) {
    // error
  } else {
    // do something else
  }
})

While callbacks are very simple in context, things get nasty if we need to do a more asynchronous operations which depend on each other. By layering callbacks upon callbacks, we may end up in the Callback Hell. Typical scenery also includes a gloomy vista upon the infamous Pyramid of Doom.

Pyramid of Doom

If you find yourself on a path to the Callback Hell, rethink your approach. Read the recommendations by Max Ogden:

  • Name your functions (instead of using anonymous functions everywhere).
  • Keep your code shallow.
  • Split your code into smaller modules.

Callbacks

If you write a library which accepts callback for parameters, stick to error-first convention. You should pass an error as the first parameter and result of the call as second.

function calculateAsync (callback) {
  try {
    // get result of some operation
    const result = calculate()
    callback(null, result)
  } catch(error) {
    // operation failed, return with error
    callback(error)
  }
}

For high-level operations with callbacks (i.e. such as resolving multiple callbacks into one), take a look at some library such as async.

Promises

A better approach may be using Promises, which let you chain asynchronous operations without nesting:

doAsync()
.then((data) => {
  return doMoreAsync()
})
.then((data) => {
  return evenMoreAsync()
})
.then((data) => {
  // we’re done
})

Native promises are only partially supported in some browsers and environments, so most applications rely on promise libraries like Bluebird which also provide some extra features. However, consider if introducing such a dependency is worth it, especially if you don’t do too much async operations.

Existing callback-based API can be easily converted to promises. Most libraries provide promisify and similar methods to convert existing callback-based functions to return promises (especially if they stick with error-first convention). Alternatively, you can write similar wrapper yourself.

Recommended Reading

The Harmful Parts

JavaScript got some bad reputation, partly for its troubled past and incompatibility across implementations in various browsers, and partly for just being a misunderstood language. Some features of JavaScript look deceptively similar to other languages (typically Java), but in JavaScript they just work different. But many times these frustrations are self-inflicted; you can easily avoid the problematic features altogether.

We particularly draw from Douglas Crockford’s recommendations, or what he calls “the good parts” of JavaScript.

Resources:

this Keyword

this in JavaScript is a typical source of frustration. Based on our familiarity with class-based languages (Java, C#, PHP…) we may assume it always points to the current object. However, the meaning of this depends on invocation of the function where it is used.

This way we can pass our objects to native methods as this:

Array.prototype.slice.call(arrayLikeObject) // Array.slice() will operate on arrayLikeObject

But hardly ever we want to settle for such behaviour in our code.

this keyword in functions defaults to the global object (i.e. window in browsers), which is extremely dangerous and lead to particularly nasty bugs:

// very bad
function setThis () {
  this.foo = 'foo set'
}
setThis()
window.foo // => 'foo set'

With strict mode this will be undefined and buggy code will crash early insted.

function setThis () {
  'use strict'
  this.foo = 'foo set'
}
setThis() // => TypeError: Cannot set property 'foo' of undefined

Typically we use this keyword in prototypal inheritance, or with ES6 classes. The issue is with asynchronous code. Typically you want to call a method on your instance from the callback, but this doesn’t point to your original instance.

Coders typically avoid these issues by closure, i.e. storing this to an outside variable:

function foo () {
  const that = this
  somethingThatRebindsThis(function() {
    that.whatever()
  })
}

It is preferred to avoid it with bind function or arrow functions in ES6 (where this is always bound to the outside scope). But as you can imagine, heavy reliance on this leads to verbose and unreadable code since we are working around JavaScript’s behaviour in attempt to make it behave like something else. We can certainly blame JavaScript for not being Java/C#/PHP/whatever, but perhaps we should blame ourselves for preferring familiarity over simplicity.

So try to avoid this. If you find yourself repeating this all over your code or fighting with binding, reconsider your approach. Perhaps you are using an inappropriate abstraction, mutating state or relying on side effects.

new Operator

We typically use new to instantiate objects in class-based languages. But technically there are no classes in JavaScript, only functions. So the new operator is instead a syntactic sugar for object instantiation, which calls your constructing function with a correct binding for this.

function Car (make, model, year) {
  this.make = make
  this.model = model
  this.year = year
}
const trabant = new Car('Trabant', '601', 1964)

Now, consider what happens if you call Car function without the new operator? If you read previous section, you probably know: this is a global object and you end-up setting properties on it – unless you use the strict mode.

const trabant = Car('Trabant')
trabant // => undefined
make // => 'Trabant'

There are techniques to guard your constructors from this misuse and ES6 transpiler can take care of necessary boilerplate code for you. But perhaps it is unnecessarily complicated to treat constructors and standard functions differently. You can easily avoid new operator by treating your constructors as simple functions which construct objects:

function Car (make, model, year) {
  return Object.freeze({
    make,
    model,
    year
  })
}

const tesla = Car('Tesla', '3', 2016)

Note the use of ES6 property shorthands. Object.freeze is optional but recommended.

But How About Performance?

Some JavaScript engines can optimise object creation with new. Additionally prototypal inheritance should have smaller memory consumption, since you don’t duplicate properties. First thing: object creation performance hardly matters for most applications, unless you need to create hundreds of objects within milliseconds. The difference can be between allocation of 2 or 30 objects within one microsecond (you have 16.67 milliseconds for each frame in a browser, which uses 60 FPS). Your biggest enemy then will be garbage collector, which can kick in any time and kill your application performance. So if you need to work with huge amounts of objects, perhaps you should use object pool to avoid object allocation in the first place. Otherwise do the sanity check.

Recommended Reading

Functional Programming

While JavaScript does not emphasise any particular programming paradigm, it goes especially well with some aspects of functional programming. Learn about functional programming and embrace it in your code. While you may have heard of some scare stories about it and/or you may think it is highly impractical stuff made up by some mathematicians, the reality is quite different.

Recommended reading:

The following section are few tips on how to start with functional style.

Separate Data and Behaviour

Put data into “dumb” structures and behaviour into functions. Mainstream “object-oriented” programming languages bundle behaviour and data into classes. Many times, however, we operate with same data in different contexts. The fabulous OOP then becomes about managing inevitable complexity as we need to do various operations with same data, but classes and inheritance are extremely poor way to manage things.

If you need to do something with your data, just put them into object or array and write a simple function to do stuff. No need to write DoStuffWithDataExecutor class to encapsulate your data. Functions in JavaScript are first-class citizens, treat them as such.

Recommended reading: Execution in the Kingdom of Nouns.

// bad
class Person {
  constructor (first, last) {
    [this.first, this.last] = [first, last]
  }
  fullName () {
    return `${this.first} ${this.last}`
  }
}

const jane = new Person('Jane', 'Doe')
jane.fullName() // => 'Jane Doe'

// good
function fullName (person) {
  return `${person.first} ${person.last}`
}

const jane = {
  first: 'Jane',
  last: 'Doe',
}

fullName(jane) // => 'Jane Doe'

Immutability

Avoid mutating state. Instead of modifying the original object, return a new object with changes.

// bad
function rename (person, newName) {
  person.name = newName
  return person
}

const jane = { name: 'Jane' }
const anna = rename(jane, 'Anna')
jane.name // => 'Anna'
anna.name // => 'Anna'

// good
function rename (person, newName) {
  const clone = Object.assign({}, person)
  clone.name = newName
  return clone
}

const jane = { name: 'Jane' }
const anna = rename(jane, 'Anna')
jane.name // => 'Jane'
anna.name // => 'Anna'

This example uses Object.assign which is not yet widely supported. Most utility libraries provide extend or clone function, or you can use some standalone implementation.

Arrays can be efficiently manipulated through standard functional methods like map or filter which return a new array.

const words = ['foobar', 'baz', 'a']
const lengths = []

// really bad
for (let i = 0; i < words.length; i++) {
  let len = words[i].length
  lengths.push(len)
}

// bad
words.forEach((word) => {
  lengths.push(word.length)
})

// good
const lengths = words.map(word => word.length)

lengths // => [6, 3, 1]

forEach usually implies a side effect, in this case it is manipulating an outside array.

If you need to manipulate larger collections, consider using advanced libraries for immutable structures like Immutable.js or Mori.

Recommended reading:

Pure Functions

Avoid side-effects and outside state. Pure functions always return the same output given the same parameters. However, since programming without side-effects would be impossible, try to contain it in dedicated functions and keep the rest of your code pure.

// impure
const majorityAge = 21

function isUnderage (age) {
  return age < majorityAge
}

// pure
function isUnderage (age) {
  const majorityAge = 21
  return age < majorityAge
}

Currying

The pure functions are easily testable and predictable. But don’t fret, you won’t need to pass every single parameter every time you call your function. With high-order functions, you can build your functions incrementally with currying.

// bad

function isUnderage (majorityAge, age) {
  return age < majorityAge
}

isUnderage(21, 19) // => true
isUnderage(18, 19) // => false
// good

function underageChecker (majorityAge) {
  return function (age) {
    return age < majorityAge
  }
}

const isUnderageUsa = underageChecker(21)
const isUnderageEu = underageChecker(18)

isUnderageUsa(19) // => true
isUnderageEu(19) // => false

// treating curried function as anonymous does not look pretty
underageChecker(21)(19) // => true

Many libraries provide universal curry function, e.g. Ramda, or you can use a stand-alone module.

// better
const underageChecker = curry((majorityAge, age) => {
  return age < majorityAge
})

const isUnderageUsa = underageChecker(21)
isUnderageUsa(19) // => true

// works the same as before
underageChecker(21)(19) // => true

// but you can also pass all the parameters
underageChecker(21, 19) // => true

Parameters Order

When currying, keep the order of parameters in mind: you want to put the most variable parameter, i.e. data, as the last parameter. This allows you to easily compose functions.

For example popular libraries Underscore and lodash put data as the first parameter.

Recommended watching: Hey Underscore, You’re Doing It Wrong!.

// bad
function firstTwoLetters (words) {
  return _.map(words, (word) => {
    return _.take(word, 2)
  })
}

firstTwoLetters(['foo', 'bar']) // => ['fo', 'ba']

On the other hand, functions in Ramda are curried and take data as the last parameter, so you can keep boilerplate to minimum.

// good
const firstTwoLetters = R.map(R.take(2))

firstTwoLetters(['foo', 'bar']) // => ['fo', 'ba']

Functional Composition

Pure functions and currying are powerful tools for manipulating data. You can compose generic functions to suit your specific needs with almost none code.

Use either compose, or pipe to combine functions into new functions. The only difference is order of functions:

Compose applies functions outside-in, i.e. compose(a, b, c)(x) is the same as a(b(c(x))); so functions are applied from right to left.

Pipe does the opposite, i.e. pipe(a, b, c)(x) is the same as c(b(a(x))); functions are applied from left to right.

While we find pipe more natural, use one , but be consistent within the project.

Try to keep your functions pointfree to make them easier to extract and reuse.

// not pointfree because we mention the data: word
function snakeCase (word) {
  return word.toLowerCase().replace(/\s+/ig, '_')
};

// pointfree, with some help from Ramda
import { toLower, replace, pipe } from 'ramda'
const snakeCase = pipe(toLower, replace(/\s+/ig, '_'))

snakeCase('Chunky Bacon') // => chunky_bacon

Acknowledgements

This guide is based upon JavaScript Quality Guide by Nicolas Bevacqua and AirBnB Style Guide. Some examples are based on Professor Frisby’s Mostly Adequate Guide to Functional Programming. Many thanks to the original authors and countless contributors.

License

MIT

Fork away!

You can’t perform that action at this time.