代码整洁之道JavaScript篇

《Clean Code》代码整洁之道, 是每个 Java 开发者编码规范的圣经。但是属于JS的代码整洁之道似乎很少有人提及,更多的只是所谓的“约定”。在github上偶遇 clean-code-javascript 后有受启发,作者主要从变量(self-documentation),函数(一个函数只做一件事),类等方面展开了论述。

变量 Variables

    1. 使用有意义语义化的变量名

不推荐示例:

const yyyymmdstr = moment().format("YYYY/MM/DD"); // 当前年-月-天的字符串

推荐示例:

 const currentDate = moment().format("YYYY/MM/DD"); // 当前日期

ps: moment.js 是一款功能非常强大的 日期/时间 库。

    1. 对于同样功能的方法使用同样的方法名,体现在不同的 js 文件中

不推荐示例:

getUserInfo(); // 获取用户信息
getClientData(); // 获取顾客数据
getCustomerRecord(); // 获取客户记录

推荐示例:

getUser(); // 获取用户,上述反例中,哪怕信息不是类似的,对用户的命名能否统一叫 user/client/customer 中的一个呢?
    1. 定义常量,让其更具语义化,更易于搜索

不推荐示例:

setTimeout(fly, 86400000); // WTF 86400000 ?

推荐示例:

const MILLSECONDS_IN_A_DAY = 86400000; // 86400000 是一天的毫秒数
setTimeout(fly, MILLSECONDS_IN_A_DAY ); // 该类常量,作者是大写加下划线的方式的格式来定义
    1. 让函数的参数传递变量,且让变量的命名更有意义

不推荐示例:

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

推荐示例:

const address = "One Infinite Loop, Cupertino 95014";
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
const [_, city, zipCode] = address.match(cityZipCodeRegex) || [];
saveCityZipCode(city, zipCode);
    1. 让遍历中的回调函数参数的变量名更直接易懂些

不推荐示例:

const areas = ["南山区", “福田区”, “罗湖区”];
areas.forEach(a => {
  buyIt(a)
})

推荐示例:

const areas = ["南山区", “福田区”, “罗湖区”];
areas.forEach(area => {
  buyIt(area)
})
    1. 通过 对象/类 来管理数据时,不必添加一些没必要的字符

不推荐示例:

const Car = {
  carMake: "BYD",
  carModel: "唐",
  carColor: "Black"
}

推荐示例:

// 已经定义为已知的 Car 类了,它的属性就不必使用 carXX 的命名了
const Car = {
  make: "BYD",
  model: "唐",
  color: "Black"
}
    1. 根据使用情境,合理使用默认值

不推荐示例:

function getUser (name) {
  const girlName = name || "Lucy";
  // ...
}

推荐示例:

function getUser (name = "Lucy") {
  // ...
}
 // 合理利用结构赋值
function getUser ({ name = 'Lucy', age = 18, sex: gender }) {}

函数 Functions

    1. 参数尽可能避免超过3个,多参数时,可以使用对象管理

不推荐示例:

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

推荐示例:

// 对象作为参数时,合理利用对象的结构赋值
function createMenu ({ title, body, buttonText, cancellable }) {
  // ...
}
createMenu({
  title: "Foo",
  body: "Bar",
  buttonText: "Baz",
  cancellable: true
})
    1. 一个函数做一件事,高复用,且更易于理解函数在做什么

不推荐示例:

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

推荐示例:

function emailActiveClients (clients) {
  clients.filter(isActiveClient).forEach(email);
}

function isActiveClient (client) {
  const clientRecord = database.lookup(client);
  return clientRecord.isActive();
}
    1. 函数的命名需要大致解释它的功能

不推荐示例:

function addToDate (date, month) {
  // ...
}
const date = new Date();
// 根据函数名,很难理解添加了什么
addToDate(date, 1);

推荐示例:

function addMonthToDate (month, date) {
  // ...
}
const date = new Date();
addMonthToDate(1, date);
    1. 将复杂的函数进行拆分,简化操作,提高复用率,便于测试。

不推荐示例:

function parseBetterJSAlternative (code) {
  const REGEXES = [
    // ...
  ];
  const statements = code.split(" ");
  const tokens = [];

  REGEXES .forEach(REGEX => {
    statements.forEach(statement => {
      tokens.push(/* ... */)
    })
  })
  const ast = [];
  tokens.forEach(token => {
    // ...
  })
  ast.forEach(node=> {
    // ...
  })
}

推荐示例:

function parseBetterJSAlternative (code) {
  const tokens = tokenize(code);
  const syntaxTress = parse(tokens);
  syntaxTree.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 parse (tokens) {
  const syntaxTree = [];
  tokens.forEach(token => {
    syntaxTree.push(/* ... */)
  })
  return syntaxTree;
}
    1. 删除重复的代码。否则涉及到变更时,需要变更多处

通常,重复的代码因为有两个或稍多个不同的事物,它们有很多共同点,但是它们之间仍有一些细微的差别,是其书写多个函数来完成许多相同的事情。删除重复代码意味着创建一个仅用一个函数、模块、类就可以处理这组不同事物的抽象。

推荐示例:

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

推荐示例:

function showEmployeeList (employees) {
  employees.forEach(employee => {
    const expectedSalary = employee.calculateExpectedSalary();
    const experience = employee.getExperience();

    const data = {
      expectedSalary,
      experience
    }

    switch (employee.type) {
      case "manager":
        data.portfolio = employee.getMBAProjects();
        break;
      case "developer":
        data.githubLink = employee.getGithubLink();
        break;
      default:
        break;
    }
    render(data);
  })
}

不推荐示例:

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)

推荐示例:

const menuConfig = {
  title: "Order",
  // 没有body属性
  buttonText: "Send",
  cancellable: true
};

function createMenu (config) {
  config = Object.assign(
    {
      title: "Foo",
      body: "Bar",
      buttonText: "Baz",
      cancellable: true
    },
    config
  )
}

createMenu(menuConfig);
    1. 当标记位作为函数的参数之一且为Bolean值时,拆分成2个函数

作者指的是temp标记位当可以用布尔值来区分(if they are following different code paths based on a boolean)时,应该拆分成单个函数。我的理解是,如果是 temp 是 type 用来区分类型,例如 redux 中的 actions/dispatch,应该不在此范畴。

不推荐示例:

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

推荐的示例:

function createFile (name) {
  fs.create(name);
}

function createTempFile (name) {
  createFile(`./temp/${name}`)
}
    1. 避免副作用(part 1)

一个函数除了接受值并返回值,或者不执行任务其他操作以外,都会产生副作用。副作用可能是写入文件,修改某些全局变量等等。现在,确实需要偶尔在程序中产生副作用,但是要避免常见的陷阱。

不推荐示例:

// name的结构被破坏,String变成了Array
let name = "Ryan McDermott";

function splitIntoFirstAndLastName () {
  name = name.split(" ");
}

splitIntoFirstAndLastName();
console.log(name); // ['Ryan', 'McDermott']

推荐示例:

function splitIntoFirstAndLastName (name) {
  return name.split(" ");
}

const name = "Ryan McDermott";
const newName = splitIntoFirstAndLastName(name);

console.log(name); // "Ryan McDermott"
console.log(newName); // ['Ryan', 'McDermott']
    1. 避免副作用(part 2)

作为函数参数时,基本类型的参数是值的传递,但是对象、数组是引用的传递。如果直接对参数进行修改,该引用的所有变量都会发生变化,造成非预期的风险。最好的做法是始终返回克隆。
警告:某些情况下,实际上可能就想修改输入对象,但是这种习惯并不好,推荐返回更没有副作用的克隆值;克隆大对象相较于直接修改输入对象,会消耗更多性能,作者推荐使用工具 Immutable.js,这个在 React 项目中经常用到。

这个可能是很多人容易忽视的,起码我在以往的项目中,遇见的太多这样的问题了。尤其还是在使用的 Vue 框架下,导致watch/bus/Dep 的触发很难定位,代码重构的非预期风险也很高。

不推荐示例:

const addItemToCart = (cart, item) => {
  // 所有cart的引用的数组均会发生变化
  cart.push({ item, date: Date.now() })
}

推荐示例:

const addItemToCart = (cart, item) => {
  return [...cart, { item, date: Date.now() }]
}
    1. 不要定义全局函数,污染全局变量在 JavaScript 中是一种不好的做法

不推荐示例:

// 可能与某些库产生冲突,发生问题后很难定位
Array.prototype.diff = function diff (comparisonArray) {
  const hash = new Set(comparisonArray);
  return this.filter(elem => !hash.has(elem));
};

推荐的示例:

class SuperArray extends Array {
  diff (comparisonArray) {
    const hash = new Set(comparisonArray);
    return this.filter(elem => !hash.has(elem));
  }
}
    1. 使用函数式编程而非命令式编程

不推荐示例:

const programmerOutput = [
  {
    name: 'Uncle Bobby',
    linesOfCode: 500
  },
  {
    name: 'Suzie Q',
    linesOfCode: 1500
  }
]

let totalOutput = 0;

for (let i = 0; i < programmerOutput.length; i++) {
  totalOutput += programmerOutput[i].linesOfCode;
}

推荐示例:

const programmerOutput = [
  {
    name: 'Uncle Bobby',
    linesOfCode: 500
  },
  {
    name: 'Suzie Q',
    linesOfCode: 1500
  }
]

const totalOutput = programmerOutput.reduce(
  (totalLines, output) =>  totalLines + output.linesOfCode,
  0
);
    1. 使用函数封装判断条件

不推荐示例:

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

推荐示例:

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

if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
  // ...
}
    1. 避免使用false判断条件

这不仅应该体现在返回函数中,应该体现在各处的单一判断中,尽可能使用true判断,因为undefined, null, false, 0, '', 转换为Boolean都是false,这可能不会是你所期望的。

不推荐示例:

function isDOMNode(node) {
  // ...
}

if (!isDOMNode(node)) {
  // ...
}

推荐示例:

function isDOMNode(node) {
  // ...
}

if (isDOMNode(node)) {
  // ...
}
    1. 避免使用条件判断

这个说法初听起来似乎莫名其妙,但这也是作者“函数只处理一件事”的体现。

不推荐示例:

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

推荐示例:

class Airplane {
  // ...
}

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();
  }
}
    1. 避免类型检查

除非使用TypeScript,或者“多态”共有的属性或方法,否则类型判断无法避免。空指针异常导致代码的阻塞,风险较大。所以对作者的该观点,各位仁者见仁吧。以下是作者提供的示例:

不推荐示例:

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

推荐示例:

function combine(val1, val2) {
  return val1 + val2;
}
    1. 不要过度优化

现代浏览器在运行时在后台进行了大量的优化。很多时候的优化,只是浪费时间。列如for循环中的遍历对象的长度的重新计算,在现代浏览器中,这是经过优化的。

    1. 删除无效代码(没什么好说的)
Tree Shaking

类 Classes

    1. 通过类 class extends 来实现继承(没什么好说的)
    1. 使用方法链

这种模式在JavaScript中非常有用,可以在jQuery和Lodash等许多库中看到它。它使得代码更具表现力,并且不再那么冗长。

不推荐示例:

class Car {
  constructor (make, model, color) {
    this.make = make;
    this.model = model;
    this.color = color;
  }
  setMake (make) {
    this.make = make;
  }
  setModel (model) {
    this.model = model;
  }
  setColor (color) {
    this.color = color;
  }
  save () {
    console.log(this.make, this.model, this.color);
  }
}

const car = new Car("Ford", "F-150", "red");
car.setColor("pink");
car.save();

推荐示例:

class Car {
  constructor (make, model, color) {
    this.make = make;
    this.model = model;
    this.color = color;
  }
  setMake (make) {
    this.make = make;
    return this;
  }
  setModel (model) {
    this.model = model;
    return this;
  }
  setColor (color) {
    this.color = color;
    return this;
  }
  save () {
    console.log(this.make, this.model, this.color);
    return this;
  }
}

// 通过返回this,实现了方法的链式调用。
const car = new Car("Ford", "F-150", "red").setColor("pink").save();

异步/并发 Async/Concurrency

    1. 使用回调Callback不如使用promise,使用resolve.then,不如使用async/await。async/await 更符合代码阅读习惯。

Callback:

import { get } from "request";
import { writeFile } from "fs";

get(
  "https://en.wikipedia.org/wiki/Robert_Cecil_Martin",
  (requestErr, response, body) => {
    if (requestErr) {
      console.error(requestErr);
    } else {
      writeFile("article.html", body, writeErr => {
        if (writeErr) {
          console.error(writeErr);
        } else {
          console.log("File written");
        }
      });
    }
  }
);

Promise:

import { get } from "request-promise";
import { writeFile } from "fs-extra";

get("https://en.wikipedia.org/wiki/Robert_Cecil_Martin")
  .then(body => {
    return writeFile("article.html", body);
  })
  .then(() => {
    console.log("File written");
  })
  .catch(err => {
    console.error(err);
  });

async/await:

import { get } from "request-promise";
import { writeFile } from "fs-extra";

async function getCleanCodeArticle() {
  try {
    const body = await get(
      "https://en.wikipedia.org/wiki/Robert_Cecil_Martin"
    );
    await writeFile("article.html", body);
    console.log("File written");
  } catch (err) {
    console.error(err);
  }
}

getCleanCodeArticle()

异常处理 Error Handling

    1. 不要不处理捕获的异常

不推荐示例:

try {
  functionThatMightThrow();
} catch (error) {
  console.log(error);
}

推荐示例:

try {
  functionThatMightThrow();
} catch (error) {
  console.error(error);
  notifyUserOfError(error);
  reportErrorToService(error);
}
    1. 不要不处理Promise中的reject状态

不推荐示例:

getdata()
  .then(data => {
    functionThatMightThrow(data);
  })
  .catch(error => {
    console.log(error);
  });

推荐示例:

getdata()
  .then(data => {
    functionThatMightThrow(data);
  })
  .catch(error => {
    console.error(error);
    notifyUserOfError(error);
    reportErrorToService(error);
  });

格式 Formatting

格式化是主观的。像本文中的许多规则一样,并没有必须遵循的一成不变的规则。要点是不要过分格式化。

    1. 使用统一的规范。使用驼峰、大小写、下划线,是主观的,但是团队要保持统一、一致。

不推荐示例:

const DAYS_IN_WEEK = 7;
const daysInMonth = 30;

const songs = ["Back In Black", "Stairway to Heaven", "Hey Jude"];
const Artists = ["ACDC", "Led Zeppelin", "The Beatles"];

function eraseDatabase() {}
function restore_database() {}

推荐示例:

const DAYS_IN_WEEK = 7;
const DAYS_IN_MONTH = 30;

const SONGS = ["Back In Black", "Stairway to Heaven", "Hey Jude"];
const ARTISTS = ["ACDC", "Led Zeppelin", "The Beatles"];

function eraseDatabase() {}
function restoreDatabase() {}
    1. 函数的调用者与被调用者的书写顺序保持在上下方。这样更符合代码的阅读习惯。

注释 Comments

    1. 仅注释业务逻辑教复杂的事物,注释并不是必需的,好的代码本身就是说明文档。
    1. 不要在代码库遗留注释过的代码,版本控制本身会保留历史代码。
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容