This article lists common bad code smell in JavaScript code, such as bidirectional data binding temporary timers, bidirectional data binding pits, complex branching statements, duplicate assignments, etc., and analyzes them such as live restore, bad code review, problem diagnosis and identification (via ESlint or other tools), code refactoring schemes, Gives first hand experience on how to write good code ~

It’s so convoluted, it’s crazy

Problem site:

If the word begins with a consonant (or consonant set), move the rest of its steps to the front and add “ay” (pig -> igpay). If the word begins with a vowel sound, keep the order but add “way” to the end (egg->eggway, etc.)

Bad code:

/* const */ var CONSONANTS = 'bcdfghjklmnpqrstvwxyz'; /* const */ var VOWELS = 'aeiou'; function englishToPigLatin(english) { /* const */ var SYLLABLE = 'ay'; var pigLatin = ''; if (english ! == null && english.length > 0 && (VOWELS.indexOf(english[0]) > -1 || CONSONANTS.indexOf(english[0]) > -1 )) { if (VOWELS.indexOf(english[0]) > -1) { pigLatin = english + SYLLABLE; } else { var preConsonants = ''; for (var i = 0; i < english.length; ++i) { if (CONSONANTS.indexOf(english[i]) > -1) { preConsonants += english[i]; if (preConsonants == 'q' && i+1 < english.length && english[i+1] == 'u') { preConsonants += 'u'; i += 2; break; } } else { break; } } pigLatin = english.substring(i) + preConsonants + SYLLABLE; } } return pigLatin; }Copy the code

What’s wrong:

Problem detected:

Lint configuration items such as maximum number of statements, complexity, maximum number of nested statements, maximum length, maximum number of arguments, maximum number of nested callbacks

/*jshint maxstatements:15, maxdepth:2, maxcomplexity:5 */
/*eslint max-statements:[2, 15], max-depth:[1, 2], complexity:[2, 5] */Copy the code

7:0 - Function 'englishToPigLatin' has a complexity of 7.
7:0 - This function has too many statements (16). Maximum allowed is 15.
22:10 - Blocks are nested too deeply (5).Copy the code

Test first:

describe('Pig Latin', function() { describe('Invalid', function() { it('should return blank if passed null', function() { expect(englishToPigLatin(null)).toBe(''); }); it('should return blank if passed blank', function() { expect(englishToPigLatin('')).toBe(''); }); it('should return blank if passed number', function() { expect(englishToPigLatin('1234567890')).toBe(''); }); it('should return blank if passed symbol', function() { expect(englishToPigLatin('~! @#$%^&*()_+')).toBe(''); }); }); describe('Consonants', function() { it('should return eastbay from beast', function() { expect(englishToPigLatin('beast')).toBe('eastbay'); }); it('should return estionquay from question', function() { expect(englishToPigLatin('question')).toBe('estionquay'); }); it('should return eethray from three', function() { expect(englishToPigLatin('three')).toBe('eethray'); }); }); describe('Vowels', function() { it('should return appleay from apple', function() { expect(englishToPigLatin('apple')).toBe('appleay'); }); }); });Copy the code

Refactored code:

const CONSONANTS = ['th', 'qu', 'b', 'c', 'd', 'f', 'g', 'h', 'j', 'k',
'l', 'm', 'n', 'p', 'q', 'r', 's', 't', 'v', 'w', 'x', 'y', 'z'];
const VOWELS = ['a', 'e', 'i', 'o', 'u'];
const ENDING = 'ay';

let isValid = word => startsWithVowel(word) || startsWithConsonant(word);
let startsWithVowel = word => !!~VOWELS.indexOf(word[0]);
let startsWithConsonant = word => !!~CONSONANTS.indexOf(word[0]);
let getConsonants = word => CONSONANTS.reduce((memo, char) => {
  if (word.startsWith(char)) {
    memo += char;
    word = word.substr(char.length);
  }
  return memo;
}, '');

function englishToPigLatin(english='') {
   if (isValid(english)) {
      if (startsWithVowel(english)) {
        english += ENDING;
      } else {
        let letters = getConsonants(english);
        english = `${english.substr(letters.length)}${letters}${ENDING}`;
      }
   }
   return english;
}Copy the code

Data comparison:

Max-statements: 16 → 6 Max-Depth: 5 → 2 Complexity: 7 → 3 Max-Len: 65 → 73 Max-params: 1 → 2 Max-nested-callbacks: 0 → 1

Related resources:

Jshint-jshint.com/ eslint-eslint.org/ jscomplexity – jscomplexity.org/ escomplex – github.com/philbooth/e… jasmine – jasmine.github.io/

Paste –

Problem site:

We need to achieve the following effect

Bad code:

var boxes = document.querySelectorAll('.Box');

[].forEach.call(boxes, function(element, index) {
  element.innerText = "Box: " + index;
  element.style.backgroundColor =
    '#' + (Math.random() * 0xFFFFFF << 0).toString(16);
});

var circles = document.querySelectorAll(".Circle");

[].forEach.call(circles, function(element, index) {
  element.innerText = "Circle: " + index;
  element.style.color =
    '#' + (Math.random() * 0xFFFFFF << 0).toString(16);
});
Copy the code

What went wrong:

Because we are paste copy!!

Problem detected:

Check out paste copy and structurally similar code snippets – jsinspect github.com/danielstjul…

Find pasted copy sections from your JS, TypeScript, C#, Ruby, CSS, HTML source code – JSCPD github.com/kucherenko/…

Refactored code:

  • Let’s pull out the random color portion…
  • Let’s pull out the weird [].forEach.call portion…
  • Let’s try to go further…
let randomColor = () => `#${(Math.random() * 0xFFFFFF << 0).toString(16)};

let $ = selector => [].slice.call(document.querySelectorAll(selector || '*'));

let updateElement = (selector, textPrefix, styleProperty) => {
  $(selector).forEach((element, index) => {
    element.innerText = textPrefix + ': ' + index;
    element.style[styleProperty] = randomColor();
  });
}

updateElement('.Box', 'Box', 'backgroundColor'); // 12: Refactored

updateElement('.Circle', 'Circle', 'color'); // 14: Refactored
Copy the code

Complex branch statements

Bad code:

function getArea(shape, options) {
  var area = 0;

  switch (shape) {
    case 'Triangle':
      area = .5 * options.width * options.height;
      break;

    case 'Square':
      area = Math.pow(options.width, 2);
      break;

    case 'Rectangle':
      area = options.width * options.height;
      break;

    default:
      throw new Error('Invalid shape: ' + shape);
  }

  return area;
}

getArea('Triangle',  { width: 100, height: 100 });
getArea('Square',    { width: 100 });
getArea('Rectangle', { width: 100, height: 100 });
getArea('Bogus');Copy the code

What went wrong:

Violate the Open /close principle:

Software elements (classes, modules, methods, etc.) should be easy to open for extension, but not more than their own modifications. The code itself can allow its behavior to be extended, but do not modify the source code

You can use things like checking: no-switch – disallow the use of the switch statement no-complex-switch-case – disallow use of complex switch statements

Refactored code:

Adding code at this point is not the same as the original switch, until it is long and smelly, and easy to break the previous code logic.

(function(shapes) { // triangle.js var Triangle = shapes.Triangle = function(options) { this.width = options.width; this.height = options.height; }; Triangle. Prototype. GetArea = function () {return 0.5. * this width. * this height. }; }(window.shapes = window.shapes || {})); function getArea(shape, options) { var Shape = window.shapes[shape], area = 0; if (Shape && typeof Shape === 'function') { area = new Shape(options).getArea(); } else { throw new Error('Invalid shape: ' + shape); } return area; } getArea('Triangle', { width: 100, height: 100 }); getArea('Square', { width: 100 }); getArea('Rectangle', { width: 100, height: 100 }); getArea('Bogus'); // circle.js (function(shapes) { var Circle = shapes.Circle = function(options) { this.radius = options.radius; }; Circle.prototype.getArea = function() { return Math.PI * Math.pow(this.radius, 2); }; Circle.prototype.getCircumference = function() { return 2 * Math.PI * this.radius; }; }(window.shapes = window.shapes || {}));Copy the code

Bad taste of magic numbers/strings

Bad code:

So Magic Strings, as you saw above, are special Strings for things like Triangle and Square.

What went wrong:

These magic numbers and strings are written directly into the code and are not easy to modify or read. Insert password.length > 9, where 9 refers to MAX_PASSWORD_SIZE. At the same time, if the judgment rule is needed in more than one place, you can avoid changing the number like 9 multiple times

En.wikipedia.org/wiki/Magic_… Stackoverflow.com/questions/4…

Refactored code:

1 Passing objects

var shapeType = { triangle: 'Triangle' // 2: Object Type }; function getArea(shape, options) { var area = 0; switch (shape) { case shapeType.triangle: // 8: Object Type area = .5 * options.width * options.height; break; } return area; } getArea(shapeType.triangle, { width: 100, height: 100 }); / / 15:Copy the code

2 Using const and symbols

const shapeType = {
  triangle: Symbol() // 2: Enum-ish
};

function getArea(shape, options) {
  var area = 0;
  switch (shape) {
    case shapeType.triangle: // 8: Enum-ishCopy the code

Code the deep

Bad code:

Person.prototype.brush = function() {
  var that = this;

  this.teeth.forEach(function(tooth) {
    that.clean(tooth);
  });

  console.log('brushed');
};
Copy the code

What went wrong:

Weird self /that/_this etc

Use esLint:

  • no-this-assign (eslint-plugin-smells)
  • consistent-this
  • no-extra-bind

Refactored code:

Bind, 2nd parameter of forEach, es6

Person.prototype.brush = function() {
  this.teeth.forEach(function(tooth) {
    this.clean(tooth);
  }.bind(this)); // 4: Use .bind() to change context
  console.log('brushed');
};

Person.prototype.brush = function() {
  this.teeth.forEach(function(tooth) {
    this.clean(tooth);
  }, this); // 4: Use 2nd parameter of .forEach to change context
  console.log('brushed');
};

Person.prototype.brush = function() {
  this.teeth.forEach(tooth => { // 2: Use ES6 Arrow Function to bind `this`
    this.clean(tooth);
  });
  console.log('brushed');
};Copy the code

Brittle character concatenation

Bad code:

var build = function(id, href, text) {
  return $( "
        
+ href + "' id='" + id + "'>" + text + "" ); }Copy the code

What went wrong:

The code is ugly, verbose, and unintuitive.

Many tools and frameworks that use ES6’s template strings (string interpolation and multi-line) also provide support for responses, such as Lodash /underscore, Angular, React, etc

Refactored code:

var build = (id, href, text) =>
  `
        
${text}
`; var build = (id, href, text) => `
${text} `;Copy the code

JQuery asked

Bad code:

$(document).ready(function() { $('.Component') .find('button') .addClass('Component-button--action') .click(function() {  alert('HEY! '); }) .end() .mouseenter(function() { $(this).addClass('Component--over'); }) .mouseleave(function() { $(this).removeClass('Component--over'); }) .addClass('initialized'); });Copy the code

What went wrong:

Too many chain calls

Refactored code:

// Event Delegation before DOM Ready $(document).on('mouseenter mouseleave', '.Component', function(e) { $(this).toggleClass('Component--over', e.type === 'mouseenter'); }); $(document).on('click', '.Component button', function(e) { alert('HEY! '); }); $(document).ready(function() { $('.Component button').addClass('Component-button--action'); });Copy the code

Temporary timer

Bad code:

setInterval(function() {
  console.log('start setInterval');
  someLongProcess(getRandomInt(2000, 4000));
}, 3000);

function someLongProcess(duration) {
  setTimeout(
    function() { console.log('long process: ' + duration); },
    duration
  );  
}

function getRandomInt(min, max) {
  return Math.floor(Math.random() * (max - min + 1)) + min;
}Copy the code

What went wrong:

Out of sync timer could not confirm timing and execution

Refactored code:

Call someLongProcess (long Process: Random time), then the setInterval fn (which is doing the long process) is iterated, or setTimeout(timer,) is passed through the callback repeatedly.

setTimeout(function timer() {
  console.log('start setTimeout')
  someLongProcess(getRandomInt(2000, 4000), function() {
    setTimeout(timer, 3000);
  });
}, 3000);

function someLongProcess(duration, callback) {
  setTimeout(function() {
    console.log('long process: ' + duration);
    callback();
  }, duration);  
}

/* getRandomInt(min, max) {} */Copy the code

Repeat the assignment

Bad code:

data = this.appendAnalyticsData(data);
data = this.appendSubmissionData(data);
data = this.appendAdditionalInputs(data);
data = this.pruneObject(data);Copy the code

What went wrong:

It’s repetitive and wordy

eslint-plugin-smells

Refactored code:

1 Nested function calls 2 forEach 3 Reduce 4 flow

// 1 data = this.pruneObject( this.appendAdditionalInputs( this.appendSubmissionData( this.appendAnalyticsData(data) ) ) ); // 2 var funcs = [ this.appendAnalyticsData, this.appendSubmissionData, this.appendAdditionalInputs, this.pruneObject ];  funcs.forEach(function(func) { data = func(data); }); Funcs = funcs.reduce(function(memo, func) {return func(memo); }, data); // 4 data = _.flow( this.appendAnalyticsData, this.appendSubmissionData, this.appendAdditionalInputs, this.pruneObject )(data);Copy the code

Bad information

Bad code:

function ShoppingCart() { this.items = []; }
ShoppingCart.prototype.addItem = function(item) {
  this.items.push(item);
};

function Product(name) { this.name = name; }
Product.prototype.addToCart = function() {
  shoppingCart.addItem(this);
};

var shoppingCart = new ShoppingCart();
var product = new Product('Socks');
product.addToCart();
console.log(shoppingCart.items);Copy the code

What went wrong:

Dependencies are tightly coupled calling each other, coupling! Such as the relationship between Product and shoppingCart

Refactored code:

1 Dependency injection 2 Message broker

function Product(name, shoppingCart) { // 6: Accept Dependency
  this.name = name;
  this.shoppingCart = shoppingCart; // 8: Save off Dependency
}
Product.prototype.addToCart = function() {
  this.shoppingCart.addItem(this);
};

var shoppingCart = new ShoppingCart();
var product = new Product('Socks', shoppingCart); // 15: Pass in Dependency
product.addToCart();
console.log(shoppingCart.items);Copy the code

var channel = postal.channel(); // 1: Broker

function ShoppingCart() {
  this.items = [];
  channel.subscribe('shoppingcart.add', this.addItem); // 5: Listen to Message
}
ShoppingCart.prototype.addItem = function(item) {
  this.items.push(item);
};

function Product(name) { this.name = name; }
Product.prototype.addToCart = function() {
  channel.publish('shoppingcart.add', this); // 13: Publish Message
};

var shoppingCart = new ShoppingCart();
var product = new Product('Socks');
product.addToCart();
console.log(shoppingCart.items);Copy the code

Continuous interaction calls

Bad code:

var search = document.querySelector('.Autocomplete');

search.addEventListener('input', function(e) {
  // Make Ajax call for autocomplete

  console.log(e.target.value);
});Copy the code

What went wrong:

It can cause lag, redundant calculations, etc

Refactored code:

Throttle and debounce


var search = document.querySelector('.Autocomplete');
search.addEventListener('input', _.throttle(function(e) {
  // Make Ajax call for autocomplete
  console.log(e.target.value);
}, 500));

var search = document.querySelector('.Autocomplete');
search.addEventListener('input', _.debounce(function(e) {
  // Make Ajax call for autocomplete
  console.log(e.target.value);
}, 500));Copy the code

Anonymous algorithm

Bad code:

var search = document.querySelector('.Autocomplete');

search.addEventListener('input', _.debounce(function(e) {
  // Make Ajax call for autocomplete

  console.log(e.target.value);
}, 500));Copy the code

What went wrong:

Anonymous functions are a good thing, but naming functions can help us:

  • Stack Trace (with Devtools and easy debug)
  • Dereferencing
  • Code Reuse

Refactored code:

var search = document.querySelector('.Autocomplete');

search.addEventListener('input', _.debounce(function matches(e) {
  console.log(e.target.value);
}, 500));Copy the code

Undefined execution

Bad code:

Specify the trigger time instead of domReady

$(document).ready(function() {
  // wire up event handlers

  // declare all the things

  // etc...
});Copy the code

What went wrong:

It’s hard to unit test

Refactored code:

Use the singleton module, plus the constructor function singleton pattern (singleton has an init method to Kick off) & constructor (new Application() -> Kick off your code in the original constructor!)

(function(myApp) { myApp.init = function() { // kick off your code }; myApp.handleClick = function() {}; // etc... }(window.myApp = window.myApp || {})); // Only include at end of main application... $(document).ready(function() { window.myApp.init(); }); var Application = (function() { function Application() { // kick off your code } Application.prototype.handleClick = function() {}; return Application; } ()); // Only include at end of main application... $(document).ready(function() { new Application(); });Copy the code

Two-way data binding

Bad code:

Take a look at any MVVM project you have (Angular, etc.)

What went wrong:

It’s Hard to track execution and data flow. (It’s also where Angular1.x gets a lot of ridicule, and React Flux.)

Refactored code:

Flux (action, dispatcher, store->view)

React Flux

An Angular2 Todo App: First look at App Development in Angular2

conclusion

For more Lint rules, find the ESlint-plugin on NPM

Reference