转载

去除 JavaScript 代码的怪味

原文:《 ELIMINATE JAVASCRIPT CODE SMELLS 》

作者: @elijahmanor

笔记:涂鸦码农

难闻的代码

/* 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; }

为毛是这个味?

很多原因:

  • 声明过多
  • 嵌套太深
  • 复杂度太高

检查工具

Lint 规则

/*jshint maxstatements:15, maxdepth:2, maxcomplexity:5 */ /*jshint 最多声明:15, 最大深度:2, 最高复杂度:5*/  /*eslint max-statements:[2, 15], max-depth:[1, 2], complexity:[2, 5] */

结果

7:0 - Function 'englishToPigLatin' has a complexity of 7. //englishToPigLatin 方法复杂度为 7 7:0 - This function has too many statements (16). Maximum allowed is 15. // 次方法有太多声明(16)。最大允许值为 15。 22:10 - Blocks are nested too deeply (5). // 嵌套太深(5)

重构

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

重构后统计

  • max-statements(最多声明): 16 → 6
  • max-depth(最大嵌套): 5 → 2
  • complexity(复杂度): 7 → 3
  • max-len(最多行数): 65 → 73
  • max-params(最多参数): 1 → 2
  • max-nested-callbacks(最多嵌套回调): 0 → 1

资源

jshint -http://jshint.com/

eslint - http://eslint.org/

jscomplexity - http://jscomplexity.org/

escomplex - https://github.com/philbooth/escomplex

jasmine - http://jasmine.github.io/

复制粘贴代码的味道

已有功能…

去除 JavaScript 代码的怪味

已有代码,BOX.js

// ... more 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); });  // ... more code ...

那么,现在想要这个功能

去除 JavaScript 代码的怪味

于是,Duang! CIRCLE.JS 就出现了…

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

为毛是这个味?因为我们复制粘贴了!

工具

JSINSPECT

检查复制粘贴和结构相似的代码

一行命令:

jsinspect

去除 JavaScript 代码的怪味

JSCPD

程序源码的复制 / 粘贴检查器

(JavaScript, TypeScript, C#, Ruby, CSS, SCSS, HTML, 等等都适用…)

jscpd -f **/*.js -l 1 -t 30 --languages javascript

去除 JavaScript 代码的怪味

怎么能不被发现?重构

把随机颜色部分丢出去…

let randomColor = () => `#${(Math.random() * 0xFFFFFF << 0).toString(16)};  let boxes = document.querySelectorAll(".Box"); [].forEach.call(boxes, (element, index) => {   element.innerText = "Box: " + index;   element.style.backgroundColor = randomColor(); });  let circles = document.querySelectorAll(".Circle"); [].forEach.call(circles, (element, index) => {   element.innerText = "Circle: " + index;   element.style.color = randomColor(); });

再重构

再把怪异的 [].forEach.call 部分丢出去…

let randomColor = () => `#${(Math.random() * 0xFFFFFF << 0).toString(16)};  let $$ = selector => [].slice.call(document.querySelectorAll(selector || '*'));  $$('.Box').forEach((element, index) => {   element.innerText = "Box: " + index;   element.style.backgroundColor = randomColor(); });  $$(".Circle").forEach((element, index) => {   element.innerText = "Circle: " + index;   element.style.color = randomColor(); });

再再重构

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');  updateElement('.Circle', 'Circle', 'color');

资源

  • jsinspect
  • jscpd
  • CodePen

switch 味道

难闻的代码

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');

为毛是这个味?违背“ 打开 / 关闭原则 ”

增加个新形状

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;      case 'Circle':       area = Math.PI * Math.pow(options.radius, 2);       break;      default:       throw new Error('Invalid shape: ' + shape);   }    return area; }

加点设计模式

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

还有其它的味道吗?神奇的字符串

function getArea(shape, options) {   var area = 0;    switch (shape) {     case 'Triangle':       area = .5 * options.width * options.height;       break;     /* ... more code ... */   }    return area; }  getArea('Triangle', { width: 100, height: 100 });

神奇的字符串重构为对象类型

var shapeType = {   triangle: 'Triangle' };  function getArea(shape, options) {   var area = 0;   switch (shape) {     case shapeType.triangle:       area = .5 * options.width * options.height;       break;   }   return area; }  getArea(shapeType.triangle, { width: 100, height: 100 });

神奇字符重构为 CONST & SYMBOLS

const shapeType = {   triangle: new Symbol() };  function getArea(shape, options) {   var area = 0;   switch (shape) {     case shapeType.triangle:       area = .5 * options.width * options.height;       break;   }   return area; }  getArea(shapeType.triangle, { width: 100, height: 100 });

工具!?!

木有 :(

ESLINT-PLUGIN-SMELLS

用于 JavaScript Smells(味道) 的 ESLint 规则

规则

  • no-switch - 不允许使用 switch 声明
  • no-complex-switch-case - 不允许使用复杂的 switch 声明

资源

  • CodePen
  • Addy Osmani’s JavaScript Design Patterns eBook
  • ESLint
  • eslint-plugin-smells
  • ES6 Scoping
  • ES6 Symbols
  • Learn ES6

this 深渊的味道

难闻的代码

function Person() {   this.teeth = [{ clean: false }, { clean: false }, { clean: false }]; };  Person.prototype.brush = function() {   var that = this;    this.teeth.forEach(function(tooth) {     that.clean(tooth);   });    console.log('brushed'); };  Person.prototype.clean = function(tooth) {   tooth.clean = true; }  var person = new Person(); person.brush(); console.log(person.teeth);

为什么是这个味?that 还是 self 还是 selfie

替换方案1) bind

Person.prototype.brush = function() {   this.teeth.forEach(function(tooth) {     this.clean(tooth);   }.bind(this));    console.log('brushed'); };

替换方案2) forEach 的第二个参数

Person.prototype.brush = function() {   this.teeth.forEach(function(tooth) {     this.clean(tooth);   }, this);    console.log('brushed'); };

替换方案3) ECMAScript 2015 (ES6)

Person.prototype.brush = function() {   this.teeth.forEach(tooth => {     this.clean(tooth);   });    console.log('brushed'); };

4a) 函数式编程

Person.prototype.brush = function() {   this.teeth.forEach(this.clean);    console.log('brushed'); };

4b) 函数式编程

Person.prototype.brush = function() {   this.teeth.forEach(this.clean.bind(this));    console.log('brushed'); };

工具

ESLint

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

字符串连接的味道

难闻的代码

var build = function(id, href) {   return $( "<div id='tab'><a href='" + href + "' id='"+ id + "'></div>" ); }

为毛是这个味?因为字符串连接

替换方案

@thomasfuchs 推文上的 JavaScript 模板引擎

function t(s, d) {   for (var p in d)     s = s.replace(new RegExp('{' + p + '}', 'g'), d[p]);   return s; }  var build = function(id, href) {   var options = {     id: id     href: href   };    return t('<div id="tab"><a href="{href}" id="{id}"></div>', options); }

替换方案2) ECMAScript 2015 (ES6) 模板字符串

var build = (id, href) =>   `<div id="tab"><a href="${href}" id="${id}"></div>`;

替换方案3) ECMAScript 2015 (ES6) 模板字符串 (多行)

替换方案4) 其它小型库或大型库 / 框架

  • Lowdash 或 Underscore
  • Angular
  • React
  • Ember
  • 等等…

工具

ESLINT-PLUGIN-SMELLS

no-complex-string-concat

资源

Tweet Sized JavaScript Templating Engine by @thomasfuchs

Learn ECMAScript 2015 (ES6) - http://babeljs.io/docs/learn-es6/

jQuery 调查

难闻的代码

$(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'); });

为毛是这个味?丧心病狂的链式调用

愉快地重构吧

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

最终 Demo

See the Pen pvQQZw by Elijah Manor ( @elijahmanor ) on CodePen .

正文到此结束
Loading...