如何提高JavaScript代码质量(一)

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

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

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

1.   变量

2.   函数

变量

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

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(client) {
  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++) {
  
// ...
}
$(function(){$('.addr').html(location.href);$('.addr').attr('href',location.href)})

你可能感兴趣的:(如何提高JavaScript代码质量(一))