難聞的程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
/* 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 規則
1 2 3 4 |
/*jshint maxstatements:15, maxdepth:2, maxcomplexity:5 */ /*jshint 最多宣告:15, 最大深度:2, 最高複雜度:5*/ /*eslint max-statements:[2, 15], max-depth:[1, 2], complexity:[2, 5] */ |
結果
1 2 3 4 5 6 |
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) |
重構
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
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/
複製貼上程式碼的味道
已有功能…
已有程式碼,BOX.js
1 2 3 4 5 6 7 8 9 10 11 |
// ... 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 ... |
那麼,現在想要這個功能
於是,Duang! CIRCLE.JS 就出現了…
1 2 3 4 5 6 7 8 9 10 11 |
// ... 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 ... |
為毛是這個味?因為我們複製貼上了!
工具
檢查複製貼上和結構相似的程式碼
一行命令:
1 |
jsinspect |
程式原始碼的複製 / 貼上檢查器
(JavaScript, TypeScript, C#, Ruby, CSS, SCSS, HTML, 等等都適用…)
1 |
jscpd -f **/*.js -l 1 -t 30 --languages javascript |
怎麼能不被發現?重構
把隨機顏色部分丟出去…
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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 部分丟出去…
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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(); }); |
再再重構
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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'); |
資源
switch 味道
難聞的程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
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'); |
為毛是這個味?違背“開啟 / 關閉原則”
增加個新形狀
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
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; } |
加點設計模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
(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'); |
再增加新形狀時
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 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 || {})); |
還有其它的味道嗎?神奇的字串
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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 }); |
神奇的字串重構為物件型別
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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 深淵的味道
難聞的程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
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
1 2 3 4 5 6 7 |
Person.prototype.brush = function() { this.teeth.forEach(function(tooth) { this.clean(tooth); }.bind(this)); console.log('brushed'); }; |
替換方案
2) forEach 的第二個引數
1 2 3 4 5 6 7 |
Person.prototype.brush = function() { this.teeth.forEach(function(tooth) { this.clean(tooth); }, this); console.log('brushed'); }; |
替換方案
3) ECMAScript 2015 (ES6)
1 2 3 4 5 6 7 |
Person.prototype.brush = function() { this.teeth.forEach(tooth => { this.clean(tooth); }); console.log('brushed'); }; |
4a) 函數語言程式設計
1 2 3 4 5 |
Person.prototype.brush = function() { this.teeth.forEach(this.clean); console.log('brushed'); }; |
4b) 函數語言程式設計
1 2 3 4 5 |
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
字串連線的味道
難聞的程式碼
1 2 3 |
var build = function(id, href) { return $( "<div id='tab'><a href='" + href + "' id='"+ id + "'></div>" ); } |
為毛是這個味?因為字串連線
替換方案
@thomasfuchs 推文上的 JavaScript 模板引擎
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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) 模板字串
1 2 |
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 調查
難聞的程式碼
1 2 3 4 5 6 7 8 9 10 |
$(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'); }); |
為毛是這個味?喪心病狂的鏈式呼叫
愉快地重構吧
1 2 3 4 5 6 7 8 9 10 11 12 |
// 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
工具
ESLINT-PLUGIN-SMELLS
難以琢磨的計時器
難聞的程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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; } |
為毛這個味?無法同步的計時器
用 setTimeout 保證順序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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) {} */ |
重複定義
難聞的程式碼
1 2 3 4 |
data = this.appendAnalyticsData(data); data = this.appendSubmissionData(data); data = this.appendAdditionalInputs(data); data = this.pruneObject(data); |
替換方案
1) 巢狀呼叫函式
1 2 3 4 5 6 7 |
data = this.pruneObject( this.appendAdditionalInputs( this.appendSubmissionData( this.appendAnalyticsData(data) ) ) ); |
2) forEach
1 2 3 4 5 6 7 8 9 10 |
var funcs = [ this.appendAnalyticsData, this.appendSubmissionData, this.appendAdditionalInputs, this.pruneObject ]; funcs.forEach(function(func) { data = func(data); }); |
3) reduce
1 2 3 4 5 6 7 8 9 10 |
var funcs = [ this.appendAnalyticsData, this.appendSubmissionData, this.appendAdditionalInputs, this.pruneObject ]; data = funcs.reduce(function(memo, func) { return func(memo); }, data); |
4) flow
1 2 3 4 5 6 |
data = _.flow( this.appendAnalyticsData, this.appendSubmissionData, this.appendAdditionalInputs, this.pruneObject )(data); |
工具
ESLINT-PLUGIN-SMELLS
資源
過度耦合
難聞的程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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); |
為毛是這個味?緊密耦合的依賴關係
如何改善!?!
- 依賴注入
- 訊息訂閱
- 依賴注入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function ShoppingCart() { this.items = []; } ShoppingCart.prototype.addItem = function(item) { this.items.push(item); }; function Product(name, shoppingCart) { this.name = name; this.shoppingCart = shoppingCart; } Product.prototype.addToCart = function() { this.shoppingCart.addItem(this); }; var shoppingCart = new ShoppingCart(); var product = new Product('Socks', shoppingCart); product.addToCart(); console.log(shoppingCart.items); |
- 訊息訂閱
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
var channel = postal.channel(); function ShoppingCart() { this.items = []; channel.subscribe('shoppingcart.add', this.addItem); } ShoppingCart.prototype.addItem = function(item) { this.items.push(item); }; function Product(name) { this.name = name; } Product.prototype.addToCart = function() { channel.publish('shoppingcart.add', this); }; var shoppingCart = new ShoppingCart(); var product = new Product('Socks'); product.addToCart(); console.log(shoppingCart.items); |
資源
- postal by @ifandelse
連續不斷的互動
難聞的程式碼
1 2 3 4 5 6 7 |
var search = document.querySelector('.Autocomplete'); search.addEventListener('input', function(e) { // Make Ajax call for autocomplete console.log(e.target.value); }); |
解決方案:節流閥
1 2 3 4 5 6 7 |
var search = document.querySelector('.Autocomplete'); search.addEventListener('input', _.throttle(function(e) { // Make Ajax call for autocomplete console.log(e.target.value); }, 500)); |
解決方案:DEBOUNCE
1 2 3 4 5 6 7 |
var search = document.querySelector('.Autocomplete'); search.addEventListener('input', _.debounce(function(e) { // Make Ajax call for autocomplete console.log(e.target.value); }, 500)); |
資源
匿名函式
難聞的程式碼
1 2 3 4 5 |
var search = document.querySelector('.Autocomplete'); search.addEventListener('input', function(e) { console.log(e.target.value); }); |
給函式命名的原因:
- 堆疊追蹤
- 去關聯
- 程式碼複用
- 堆疊追蹤
1 2 3 4 5 |
var search = document.querySelector('.Autocomplete'); search.addEventListener('input', function(e) { console.log(e.target.value); }); |
修改後
1 2 3 4 5 |
var search = document.querySelector('.Autocomplete'); search.addEventListener('input', function matches(e) { console.log(e.target.value); }); |
- 去關聯
單次事件繫結
1 2 3 4 5 |
document.querySelector('button') .addEventListener('click', function handler() { alert('Ka-boom!'); this.removeEventListener('click', handler); }); |
- 程式碼複用
1 2 3 4 5 |
var kaboom = function() { alert('Ka-boom'); }; document.querySelector('button').addEventListener('click', kaboom); document.querySelector('#egg').addEventListener('mouseenter', kaboom); |
資源
結尾
更多的 ESLint 規則
資源
NPM 搜尋 eslint-plugin
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式