代码简洁之道

摘要: 可以说是《Clean Code》的JS代码示例了,值得参考。

  • 原文:JavaScript 代码简洁之道
  • 作者:缪宇

Fundebug经授权转载,版权归原作者所有。

测试代码质量的唯一方式:别人看你代码时说 f * k 的次数。

代码质量与其整洁度成正比。干净的代码,既在质量上较为可靠,也为后期维护、升级奠定了良好基础。

本文并不是代码风格指南,而是关于代码的可读性复用性扩展性探讨。

我们将从几个方面展开讨论:

  • 变量
  • 函数
  • 对象和数据结构
  • SOLID
  • 测试
  • 异步
  • 错误处理
  • 代码风格
  • 注释

变量

用有意义且常用的单词命名变量

Bad:

 const yyyymmdstr = moment().format('YYYY/MM/DD');

Good:

 const currentDate = moment().format('YYYY/MM/DD');

保持统一

可能同一个项目对于获取用户信息,会有三个不一样的命名。应该保持统一,如果你不知道该如何取名,可以去 codelf 搜索,看别人是怎么取名的。

Bad:

getUserInfo();
getClientData(); getCustomerRecord();

Good

getUser()

每个常量都该命名

可以用 buddy.js 或者 ESLint 检测代码中未命名的常量。

Bad:

// 三个月之后你还能知道 86400000 是什么吗?
setTimeout(blastOff, 86400000);

Good

const MILLISECOND_IN_A_DAY = 86400000; setTimeout(blastOff, MILLISECOND_IN_A_DAY);

可描述

通过一个变量生成了一个新变量,也需要为这个新变量命名,也就是说每个变量当你看到他第一眼你就知道他是干什么的。

Bad:

const ADDRESS = 'One Infinite Loop, Cupertino 95014'; const CITY_ZIP_CODE_REGEX = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/; saveCityZipCode(ADDRESS.match(CITY_ZIP_CODE_REGEX)[1], address.match(CITY_ZIP_CODE_REGEX)[2]);

Good

const ADDRESS = 'One Infinite Loop, Cupertino 95014'; const CITY_ZIP_CODE_REGEX = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/; const [, city, zipCode] = ADDRESS.match(CITY_ZIP_CODE_REGEX) || []; saveCityZipCode(city, zipCode);

直接了当

Bad:

const locations = ['Austin', 'New York', 'San Francisco']; locations.forEach((l) => { doStuff(); doSomeOtherStuff(); // ... // ... // ... // 需要看其他代码才能确定 'l' 是干什么的。 dispatch(l); });

Good

const locations = ['Austin', 'New York', 'San Francisco']; locations.forEach((location) => { doStuff(); doSomeOtherStuff(); // ... // ... // ... dispatch(location); });

避免无意义的前缀

如果创建了一个对象 car,就没有必要把它的颜色命名为 carColor。

Bad:

const car = { carMake: 'Honda', carModel: 'Accord', carColor: 'Blue' }; function paintCar(car) { car.carColor = 'Red'; }

Good

const car = { make: 'Honda', model: 'Accord', color: 'Blue' }; function paintCar(car) { car.color = 'Red'; }

使用默认值

Bad:

function createMicrobrewery(name) { const breweryName = name || 'Hipster Brew Co.'; // ... }

Good

function createMicrobrewery(name = 'Hipster Brew Co.') { // ... }

函数

参数越少越好

如果参数超过两个,使用 ES2015/ES6 的解构语法,不用考虑参数的顺序。

Bad:

function createMenu(title, body, buttonText, cancellable) { // ... }

Good

function createMenu({ title, body, buttonText, cancellable }) { // ... } createMenu({ title: 'Foo', body: 'Bar', buttonText: 'Baz', cancellable: true });

只做一件事情

这是一条在软件工程领域流传久远的规则。严格遵守这条规则会让你的代码可读性更好,也更容易重构。如果违反这个规则,那么代码会很难被测试或者重用。

Bad:

function emailClients(clients) { clients.forEach((client) => { const clientRecord = database.lookup(client); if (clientRecord.isActive()) { email(client); } }); }

Good

function emailActiveClients(clients) { clients .filter(isActiveClient) .forEach(email); } function isActiveClient() { const clientRecord = database.lookup(client); return clientRecord.isActive(); }

顾名思义

看函数名就应该知道它是干啥的。

Bad:

function addToDate(date, month) { // ... } const date = new Date(); // 很难知道是把什么加到日期中 addToDate(date, 1);

Good

function addMonthToDate(month, date) { // ... } const date = new Date(); addMonthToDate(1, date);

只需要一层抽象层

如果函数嵌套过多会导致很难复用以及测试。

Bad:

function parseBetterJSAlternative(code) { const REGEXES = [ // ... ]; const statements = code.split(' '); const tokens = []; REGEXES.forEach((REGEX) => { statements.forEach((statement) => { // ... }); }); const ast = []; tokens.forEach((token) => { // lex... }); ast.forEach((node) => { // parse... }); }

Good

function parseBetterJSAlternative(code) { const tokens = tokenize(code); const ast = lexer(tokens); ast.forEach((node) => { // parse... }); } function tokenize(code) { const REGEXES = [ // ... ]; const statements = code.split(' '); const tokens = []; REGEXES.forEach((REGEX) => { statements.forEach((statement) => { tokens.push( /* ... */ ); }); }); return tokens; } function lexer(tokens) { const ast = []; tokens.forEach((token) => { ast.push( /* ... */ ); }); return ast; }

删除重复代码

很多时候虽然是同一个功能,但由于一两个不同点,让你不得不写两个几乎相同的函数。

要想优化重复代码需要有较强的抽象能力,错误的抽象还不如重复代码。所以在抽象过程中必须要遵循 SOLID 原则(SOLID 是什么?稍后会详细介绍)。

Bad:

function showDeveloperList(developers) { developers.forEach((developer) => { const expectedSalary = developer.calculateExpectedSalary(); const experience = developer.getExperience(); const githubLink = developer.getGithubLink(); const data = { expectedSalary, experience, githubLink }; render(data); }); } function showManagerList(managers) { managers.forEach((manager) => { const expectedSalary = manager.calculateExpectedSalary(); const experience = manager.getExperience(); const portfolio = manager.getMBAProjects(); const data = { expectedSalary, experience, portfolio }; render(data); }); }

Good

function showEmployeeList(employees) { employees.forEach(employee => { const expectedSalary = employee.calculateExpectedSalary(); const experience = employee.getExperience(); const data = { expectedSalary, experience, }; switch(employee.type) { case 'develop': data.githubLink = employee.getGithubLink(); break case 'manager': data.portfolio = employee.getMBAProjects(); break } render(data); }) }

对象设置默认属性

Bad:

const menuConfig = { title: null, body: 'Bar', buttonText: null, cancellable: true }; function createMenu(config) { config.title = config.title || 'Foo'; config.body = config.body || 'Bar'; config.buttonText = config.buttonText || 'Baz'; config.cancellable = config.cancellable !== undefined ? config.cancellable : true; } createMenu(menuConfig);

Good

const menuConfig = { title: 'Order', // 'body' key 缺失 buttonText: 'Send', cancellable: true }; function createMenu(config) { config = Object.assign({ title: 'Foo', body: 'Bar', buttonText: 'Baz', cancellable: true }, config); // config 就变成了: {title: "Order", body: "Bar", buttonText: "Send", cancellable: true} // ... } createMenu(menuConfig);

不要传 flag 参数

通过 flag 的 true 或 false,来判断执行逻辑,违反了一个函数干一件事的原则。

Bad:

function createFile(name, temp) { if (temp) { fs.create(`./temp/${name}`); } else { fs.create(name); } }

Good

function createFile(name) { fs.create(name); } function createFileTemplate(name) { createFile(`./temp/${name}`) }

避免副作用(第一部分)

函数接收一个值返回一个新值,除此之外的行为我们都称之为副作用,比如修改全局变量、对文件进行 IO 操作等。 当函数确实需要副作用时,比如对文件进行 IO 操作时,请不要用多个函数/类进行文件操作,有且仅用一个函数/类来处理。也就是说副作用需要在唯一的地方处理。 副作用的三大天坑:随意修改可变数据类型、随意分享没有数据结构的状态、没有在统一地方处理副作用。

Bad:

// 全局变量被一个函数引用
// 现在这个变量从字符串变成了数组,如果有其他的函数引用,会发生无法预见的错误。
var name = 'Ryan McDermott'; function splitIntoFirstAndLastName() { name = name.split(' '); } splitIntoFirstAndLastName(); console.log(name); // ['Ryan', 'McDermott'];

Good

var name = 'Ryan McDermott'; var newName = splitIntoFirstAndLastName(name) function splitIntoFirstAndLastName(name) { return name.split(' '); } console.log(name); // 'Ryan McDermott'; console.log(newName); // ['Ryan', 'McDermott'];

避免副作用(第二部分)

在 JavaScript 中,基本类型通过赋值传递,对象和数组通过引用传递。以引用传递为例: 假如我们写一个购物车,通过 addItemToCart() 方法添加商品到购物车,修改 购物车数组。此时调用 purchase() 方法购买,由于引用传递,获取的 购物车数组 正好是最新的数据。

看起来没问题对不对?

如果当用户点击购买时,网络出现故障, purchase() 方法一直在重复调用,与此同时用户又添加了新的商品,这时网络又恢复了。那么 purchase() 方法获取到 购物车数组 就是错误的。

为了避免这种问题,我们需要在每次新增商品时,克隆 购物车数组 并返回新的数组。

Bad:

const addItemToCart = (cart, item) => { cart.push({ item, date: Date.now() }); };

Good

const addItemToCart = (cart, item) => { return [...cart, {item, date: Date.now()}] };

不要写全局方法

在 JavaScript 中,永远不要污染全局,会在生产环境中产生难以预料的 bug。举个例子,比如你在 Array.prototype 上新增一个 diff 方法来判断两个数组的不同。而你同事也打算做类似的事情,不过他的 diff 方法是用来判断两个数组首位元素的不同。很明显你们方法会产生冲突,遇到这类问题我们可以用 ES2015/ES6 的语法来对 Array 进行扩展。 Bad:

Array.prototype.diff = function diff(comparisonArray) { const hash = new Set(comparisonArray); return this.filter(elem => !hash.has(elem)); };

Good

class SuperArray extends Array { diff(comparisonArray) { const hash = new Set(comparisonArray); return this.filter(elem => !hash.has(elem)); } }

比起命令式我更喜欢函数式编程

函数式变编程可以让代码的逻辑更清晰更优雅,方便测试。

Bad:

const programmerOutput = [ { name: 'Uncle Bobby', linesOfCode: 500 }, { name: 'Suzie Q', linesOfCode: 1500 }, { name: 'Jimmy Gosling', linesOfCode: 150 }, { name: 'Gracie Hopper', linesOfCode: 1000 } ]; let totalOutput = 0; for (let i = 0; i < programmerOutput.length; i++) { totalOutput += programmerOutput[i].linesOfCode; }

Good

const programmerOutput = [ { name: 'Uncle Bobby', linesOfCode: 500 }, { name: 'Suzie Q', linesOfCode: 1500 }, { name: 'Jimmy Gosling', linesOfCode: 150 }, { name: 'Gracie Hopper', linesOfCode: 1000 } ]; let totalOutput = programmerOutput .map(output => output.linesOfCode) .reduce((totalLines, lines) => totalLines + lines, 0)

封装条件语句

Bad:

if (fsm.state === 'fetching' && isEmpty(listNode)) { // ... }

Good

function shouldShowSpinner(fsm, listNode) { return fsm.state === 'fetching' && isEmpty(listNode); } if (shouldShowSpinner(fsmInstance, listNodeInstance)) { // ... }

尽量别用“非”条件句

Bad:

function isDOMNodeNotPresent(node) { // ... } if (!isDOMNodeNotPresent(node)) { // ... }

Good

function isDOMNodePresent(node) { // ... } if (isDOMNodePresent(node)) { // ... }

避免使用条件语句

Q:不用条件语句写代码是不可能的。 A:绝大多数场景可以用多态替代。 Q:用多态可行,但为什么就不能用条件语句了呢? A:为了让代码更简洁易读,如果你的函数中出现了条件判断,那么说明你的函数不止干了一件事情,违反了函数单一原则。

Bad:

class Airplane { // ... // 获取巡航高度 getCruisingAltitude() { switch (this.type) { case '777': return this.getMaxAltitude() - this.getPassengerCount(); case 'Air Force One': return this.getMaxAltitude(); case 'Cessna': return this.getMaxAltitude() - this.getFuelExpenditure(); } } }

Good

class Airplane { // ... } // 波音777 class Boeing777 extends Airplane { // ... getCruisingAltitude() { return this.getMaxAltitude() - this.getPassengerCount(); } } // 空军一号 class AirForceOne extends Airplane { // ... getCruisingAltitude() { return this.getMaxAltitude(); } } // 赛纳斯飞机 class Cessna extends Airplane { // ... getCruisingAltitude() { return this.getMaxAltitude() - this.getFuelExpenditure(); } }

避免类型检查(第一部分)

JavaScript 是无类型的,意味着你可以传任意类型参数,这种自由度很容易让人困扰,不自觉的就会去检查类型。仔细想想是你真的需要检查类型还是你的 API 设计有问题?

Bad:

function travelToTexas(vehicle) { if (vehicle instanceof Bicycle) { vehicle.pedal(this.currentLocation, new Location('texas')); } else if (vehicle instanceof Car) { vehicle.drive(this.currentLocation, new Location('texas')); } }

Good

function travelToTexas(vehicle) { vehicle.move(this.currentLocation, new Location('texas')); }

避免类型检查(第二部分)

如果你需要做静态类型检查,比如字符串、整数等,推荐使用 TypeScript,不然你的代码会变得又臭又长。

Bad:

function combine(val1, val2) { if (typeof val1 === 'number' && typeof val2 === 'number' || typeof val1 === 'string' && typeof val2 === 'string') { return val1 + val2; } throw new Error('Must be of type String or Number'); }

Good

function combine(val1, val2) { return val1 + val2; }

不要过度优化

现代浏览器已经在底层做了很多优化,过去的很多优化方案都是无效的,会浪费你的时间,想知道现代浏览器优化了哪些内容,请点这里。

Bad:

// 在老的浏览器中,由于 `list.length` 没有做缓存,每次迭代都会去计算,造成不必要开销。
// 现代浏览器已对此做了优化。
for (let i = 0, len = list.length; i < len; i++) { // ... }

Good

for (let i = 0; i < list.length; i++) { // ... }

删除弃用代码

很多时候有些代码已经没有用了,但担心以后会用,舍不得删。

如果你忘了这件事,这些代码就永远存在那里了。

放心删吧,你可以在代码库历史版本中找他它。

Bad:

function oldRequestModule(url) { // ... } function newRequestModule(url) { // ... } const req = newRequestModule; inventoryTracker('apples', req, 'www.inventory-awesome.io');

Good

function newRequestModule(url) { // ... } const req = newRequestModule; inventoryTracker('apples', req, 'www.inventory-awesome.io');

对象和数据结构

用 get、set 方法操作数据

这样做可以带来很多好处,比如在操作数据时打日志,方便跟踪错误;在 set 的时候很容易对数据进行校验…

Bad:

function makeBankAccount() { // ... return { balance: 0, // ... }; } const account = makeBankAccount(); account.balance = 100;

Good

function makeBankAccount() { // 私有变量 let balance = 0; function getBalance() { return balance; } function setBalance(amount) { // ... 在更新 balance 前,对 amount 进行校验 balance = amount; } return { // ... getBalance, setBalance, }; } const account = makeBankAccount(); account.setBalance(100);

使用私有变量

可以用闭包来创建私有变量

Bad:

const Employee = function(name) { this.name = name; }; Employee.prototype.getName = function getName() { return this.name; }; const employee = new Employee('John Doe'); console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe delete employee.name; console.log(`Employee name: ${employee.getName()}`); // Employee name: undefined

Good

function makeEmployee(name) { return { getName() { return name; }, }; } const employee = makeEmployee('John Doe'); console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe delete employee.name; console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe

使用 class

在 ES2015/ES6 之前,没有类的语法,只能用构造函数的方式模拟类,可读性非常差。

Bad:

// 动物
const Animal = function(age) { if (!(this instanceof Animal)) { throw new Error('Instantiate Animal with `new`'); } this.age = age; }; Animal.prototype.move = function move() {}; // 哺乳动物 const Mammal = function(age, furColor) { if (!(this instanceof Mammal)) { throw new Error('Instantiate Mammal with `new`'); } Animal.call(this, age); this.furColor = furColor; }; Mammal.prototype = Object.create(Animal.prototype); Mammal.prototype.constructor = Mammal; Mammal.prototype.liveBirth = function liveBirth() {}; // 人类 const Human = function(age, furColor, languageSpoken) { if (!(this instanceof Human)) { throw new Error('Instantiate Human with `new`'); } Mammal.call(this, age, furColor); this.languageSpoken = languageSpoken; }; Human.prototype = Object.create(Mammal.prototype); Human.prototype.constructor = Human; Human.prototype.speak = function speak

转载于:https://www.cnblogs.com/ckAng/p/10280589.html

你可能感兴趣的:(代码简洁之道)