Tips for writing cleaner JavaScript

Presented by Carlos Justiniano / HC.JS / @cjus / ver: 1.1.0

About this presentation

  • Focus on practical tips for writing cleaner code
  • We'll look at actual before and after code samples
  • We'll also highlight tools which can help us write cleaner code

Presentation source at: http://github.com/cjus/writing-cleaner-js-presentation
Issues with this presentation? Fix it and issue a pull request

About me

  • Carlos Justiniano, HC.JS founder and a long-time programmer
  • I'm into full-stack JS development
  • I've consulted for numerous companies on commercial JS based products

What is clean code?

  • Code that is reasonably understandable
  • Code free of side effects
  • Code that is testable

AWARENESS IS HALF THE BATTLE

Identifying and labeling bad code

  • Poorly written code
  • Mental context switching and code complexity
  • Tech / code debt

Poorly written code

  • Very few of us set out to write poor code
  • In many environments business pressures stress code quality
  • Often clean code degrades to poor code over time

Mental context switches and code complexity

  • In computing a context switch is the basis of multitasking
  • In humans, a mental context occurs when you have to switch between various program states as you attempt to understand what a program is doing
  • The more complicated the code the more difficult the mental context switches
  • Definite correlation between the number of context switches and code complexity
  • Takeaway: seek to simplify

Technical debt ... referring to the eventual consequences of poor system design, software architecture or software development within a codebase. - Wikipedia

Why should you care?

  • Often the first person who has to deal with dirty code is the person who wrote it
  • Then others may have to deal - think Karma
  • There's a real cost to dealing with dirty code

QUICK TIPS

Quick tips for writing cleaner code

  • Write consistent looking code. Embrace a style guide.
  • Take care in naming your variables, functions and objects
  • Don't bring your Ruby, Python or {your_language_here} naming conventions - use JS naming conventions... i.e. camelCase, CapitalizeConstructorNames
  • Favor small functions and objects
  • Limit the number of lines of code in a file or module

To comment or not to comment?

  • It's the subject of some debate
  • We can agree that useless comments are worse than no comments
  • Con: Comment take work and can get out of date
  • Pro: Use comments to share an understanding that can't be easily be inferred from the code
  • Pro: Well structured comments can make the code easier to understand by providing visual separators
  • Tip: Consider JSDoc (http://usejsdoc.org/)

Example: JSDoc and Webstorm


  /**
   * transSingleExecute
   * Execute a single transaction and handle commit or
   * rollback, return promise
   * @param transobj {object} - transaction object
   * @param statements {array} - transactions statements
   * @returns {promise|*|Q.promise}
   */
  function transSingleExecute(transobj, statements) {
    // :
    return deferred.promise;
  }

Don't use magic numbers

  • Magic numbers are numbers that appear in code which seem to come from nowhere
//Before
  var ageInYears = 28, daysOld;
  daysOld = ageInYears * 365;
//After
  var ageInYears = 28, daysOld, daysInYear = 365;
  daysOld = ageInYears * daysInYear;

Don't use single line if statements

//Before
  if (!this.loadedSubViews) { this.loadedSubViews = []; }
//After
  if (!this.loadedSubViews) {
    this.loadedSubViews = [];
  }

Always include curly braces

//Before
  if (!this.loadedSubViews)
    this.loadedSubViews = [];
//After
  if (!this.loadedSubViews) {
    this.loadedSubViews = [];
  }

TIPS FOR VARIABLES

Remember variable hoisting

  • In JavaScript variable declarations are handled before variable assignments
//Before
  function foo() {
    var pi = 3.14159265358979323846264338327950288419716;
    var radius = 12;

    console.log('PI, radius: ', pi, radius);
    var circumference = 2 * pi * radius;
    console.log('Circumference: ', circumference);
  }
//After
  function foo() {
    var pi;
    var radius;
    var circumference;

    pi = 3.14159265358979323846264338327950288419716;
    radius = 12;
    console.log('PI, radius: ', pi, radius);
    circumference = 2 * pi * radius;
    console.log('Circumference: ', circumference);
  }

Move variable declarations to the top of their function scope

//approach #1
  function foo() {
    var pi, radius, circumference;

    pi = 3.14159265358979323846264338327950288419716;
    radius = 12;
    console.log('PI, radius: ', pi, radius);
    circumference = 2 * pi * radius;
    console.log('Circumference: ', circumference);
  }
//approach #2
  function foo() {
    var pi = 3.14159265358979323846264338327950288419716,
        radius = 12,
        circumference = 2 * pi * radius;

    console.log('PI, radius: ', pi, radius);
    console.log('Circumference: ', circumference);
  }

Avoid single var abuse #1

//Before
  function foo() {
    var pi = 3.14159265358979323846264338327950288419716,
        radius = 12,
        circumference = 2 * pi * radius,
        largerCircleCircumference = circumference * 10;

    console.log('Larger circle circumference: ', largerCircleCircumference);
  }
//After
  function foo() {
    var pi = 3.14159265358979323846264338327950288419716,
        radius = 12,
        circumference,
        largerCircleCircumference;
    circumference = 2 * pi * radius,
    largerCircleCircumference = circumference * 10;
    console.log('Larger circle circumference: ', largerCircleCircumference);
  }

Avoid single var abuse #2

//Before
function foo() {
  var pi = 3.14159265358979323846264338327950288419716,
      radius = 12,
      circumference,
      largerCircleCircumference;

  circumference = 2 * pi * radius;
  largerCircleCircumference = circumference * 10;
  console.log('Larger circle circumference: ', largerCircleCircumference);
}
//adding function declarations
function foo() {
  var pi = 3.14159265358979323846264338327950288419716,
      radius = 12,
      circumference,
      computeLargerCircle = function(factor) {
        return circumference * 10;
      };

  circumference = 2 * pi * radius;
  console.log('computeLargerCircle', computeLargerCircle);
  console.log('Larger circle circumference: ', computeLargerCircle(10));
}

Avoid single var abuse #3

//Not great
function foo() {
  var pi = 3.14159265358979323846264338327950288419716,
      radius = 12,
      circumference = function(radius) {
        return 2 * pi * radius;
      },
      biggerCircle = function() {
        return circumference(radius) * 2;
      };
  console.log('Bigger circle circumference: ', biggerCircle());
}

Avoid single var abuse #4

//Not great
  function foo() {
    var pi = 3.14159265358979323846264338327950288419716,
        radius = 12,
        circumference = function(radius) {
          return 2 * pi * radius;
        },
        biggerCircle = function() {
          return circumference(radius) * 2;
        },
        evenBiggerCircle = biggerCircle() * 2;

    console.log('Bigger circle circumference: ', biggerCircle());
    console.log('Even bigger circle circumference: ', evenBiggerCircle);
  }

You don't have to assign vars right away


  function foo() {
    var pi, radius, evenBiggerCircle;

    function circumference(radius) {
      return 2 * pi * radius;
    }

    function biggerCircle() {
      return circumference(radius) * 2;
    }

    pi = 3.14159265358979323846264338327950288419716;
    radius = 12;
    evenBiggerCircle = biggerCircle() * 2;

    console.log('Bigger circle circumference: ', biggerCircle());
    console.log('Even bigger circle circumference: ', evenBiggerCircle);
  }

TIPS FOR CONDITIONALS

Avoid large conditional blocks #1

//Before
var getUserIDsToNotify = function(notifyTo, data) {
  'use strict';
  var userIDs = [];
  var deferred = q.defer();

  if (notifyTo === 'USERS') {
    if (!data.user_ids) {
      deferred.reject({success: false});
    } else {
      userIDs = data.user_ids;
      deferred.resolve(userIDs);
    }
  } else if (notifyTo === 'GROUP') {
    if (!data.group_id) {
      deferred.reject({success: false});
    } else {
      groupLib.listGroupMembers(data.group_id)
        .then(function(results) {
          for (var index in results) {
            userIDs.push(results[index].user_id);
          }

          deferred.resolve(userIDs);
        })
        .catch(function(error) {
          deferred.reject(error);
        });
    }
  } else if (notifyTo === 'ALL_GROUPS') {
    // get all group ids and then users OR get all the users
    groupLib.listAllGroupMembers()
      .then(function(results) {
        for (var index in results) {
          userIDs.push(results[index].user_id);
        }

        deferred.resolve(userIDs);
      })
      .catch(function(error) {
        deferred.reject(error);
      });
  }
  return deferred.promise;
};

Avoid large conditional blocks #2

//After
  function handleUsers(data, deferred) {
    var userIDs = [];
    if (!data.user_ids) {
        deferred.reject({success: false});
      } else {
        userIDs = data.user_ids;
        deferred.resolve(userIDs);
      }
  }

  function handleGroup(data, deferred) {
    var userIDs = [];
    if (!data.group_id) {
      deferred.reject({success: false});
    } else {
      groupLib.listGroupMembers(data.group_id)
        .then(function(results) {
          for (var index in results) {
            userIDs.push(results[index].user_id);
          }
          deferred.resolve(userIDs);
        })
        .catch(function(error) {
          deferred.reject(error);
        });
    }
  }

  function handleAllGroups(data, deferred) {
    var userIDs = [];
    groupLib.listAllGroupMembers()
      .then(function(results) {
        for (var index in results) {
          userIDs.push(results[index].user_id);
        }
        deferred.resolve(userIDs);
      })
      .catch(function(error) {
        deferred.reject(error);
      });
  }

  function getUserIDsToNotify(notifyTo, data) {
    'use strict';
    var deferred = q.defer();

    if (notifyTo === 'USERS') {
      handleUsers(data, deferred);
    } else if (notifyTo === 'GROUP') {
      handleGroup(data, deferred);
    } else if (notifyTo === 'ALL_GROUPS') {
      handleAllGroups(data, deferred);
    }

    return deferred.promise;
  };

TIPS FOR FUNCTIONS

Move nested functions to the top of the enclosing function after the var declarations


  function foo() {
    var pi, radius, evenBiggerCircle;

    function circumference(radius) {
      return 2 * pi * radius;
    }

    function biggerCircle() {
      return circumference(radius) * 2;
    }

    pi = 3.14159265358979323846264338327950288419716;
    radius = 12;
    evenBiggerCircle = biggerCircle() * 2;

    console.log('Bigger circle circumference: ', biggerCircle());
    console.log('Even bigger circle circumference: ', evenBiggerCircle);
  }

Name your anonymous functions

//Before
  setTimeout(function() { console.log("Hello") }, 3000);
  setTimeout(function() { console.log("World") }, 1000);
//After
  setTimeout(function Hello() { console.log("Hello") }, 3000);
  setTimeout(function World() { console.log("World") }, 1000);

Avoid long function chains

//Not great
  function foo() {
    var pi = 3.14159265358979323846264338327950288419716,
        radius = 12,
        circumference = function(radius) {
          return 2 * pi * radius;
        },
        biggerCircle = function() {
          return circumference(radius) * 2;
        },
        evenBiggerCircle = biggerCircle() * 2;

    console.log('Bigger circle circumference: ', biggerCircle());
    console.log('Even bigger circle circumference: ', evenBiggerCircle);
  }

Avoid too many vars in a function

  • Too many vars in a function is what we call a code smell
  • A sign that things are not quite right in there
  • Also consider using arrays

Favor passing objects rather than a list of parameters

function drawBanner(text, x, y, color, fontSize, underline, bold) {}
//Usage
  drawBanner('Hello World', 100,100, 24, false, true);
//After
  var helloWorldBanner;
  function drawBanner(banner) {}

  helloWorldBanner = {
    text: 'Hellow World',
    x: 100,
    y: 100,
    fontSize: 24,
    underline: false,
    bold: true;
  };
  drawBanner(helloWorldBanner);

Code clustering and flow

Code clustering and flow

  • Code clustering is about grouping related code in order to make it easier to work with
  • It's about code organization at the declaration stage
  • Flow is about organizing program flow so that related functionality executes in a logical and easy to understand manner

TIPS FOR REFACTORING

TIPS FOR REFACTORING

  • Refactoring is about improving code often without changing what it does
  • Refactoring can be error prone if you don't have tests for your code
  • This is one of the many important reasons for adding tests to your project
  • Code which has tests is easier to refactor, because you can confirm that the code worked before you changed it
  • Refactored code should be easier to understand and maintain - if not you're not refactoring you're doing something like perhaps optimizing

Tools for writing cleaner code

  • Embrace consistency. Adopt a code style guide.
  • Lots to choose from, see this one to start: https://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml
  • Use Linters and style checkers. See: JSHint, JSCS

Contact

  • cjus on Twitter and Github
  • Email: cjus34@gmail.com
  • About: http://cjus.me