函数
函数参数 (两个以下最理想)
限制函数参数的个数是非常重要的, 因为这样将使你的函数容易进行测试。 一旦超过三个参数将会导致组合爆炸, 因为你不得不编写大量针对每个参数的测试用例。
没有参数是最理想的, 一个或者两个参数也是可以的, 三个参数应该避免, 超过三个应该被重构。 通常,如果你有一个超过两个函数的参数, 那就意味着你的函数尝试做太多的事情。 如果不是, 多数情况下一个更高级对象可能会满足需求。
由于 JavaScript 允许我们不定义类型/模板就可以创建对象, 当你发现你自己需要大量的参数时, 你可以使用一个对象。
不好的:
function createMenu(title, body, buttonText, cancellable) {
// ...
}
好的:
const menuConfig = {
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
};
function createMenu(config) {
// ...
}
函数应当只做一件事情
这是软件工程中最重要的一条规则, 当函数需要做更多的事情时, 它们将会更难进行编写、 测试和推理。当你能将一个函数隔离到只有一个动作, 他们将能够被容易的进行重构并且你的代码将会更容易阅读。 如果你严格遵守本指南中的这一条, 你将会领先于许多开发者。
不好的:
function emailClients(clients) {
clients.forEach((client) => {
const clientRecord = database.lookup(client);
if (clientRecord.isActive()) {
email(client);
}
});
}
好的:
function emailClients(clients) {
clients
.filter(isClientActive)
.forEach(email);
}
function isClientActive(client) {
const clientRecord = database.lookup(client);
return clientRecord.isActive();
}
函数名称应该说明它要做什么
不好的:
function addToDate(date, month) {
// ...
}
const date = new Date();
// 很难从函数名看出加了什么
addToDate(date, 1);
好的:
function addMonthToDate(month, date) {
// ...
}
const date = new Date();
addMonthToDate(1, date);
函数应该只有一个抽象级别
当在你的函数中有多于一个抽象级别时, 你的函数通常做了太多事情。 拆分函数将会提升重用性和测试性。
不好的:
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...
});
}
好的:
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;
}
function parseBetterJSAlternative(code) {
const tokens = tokenize(code);
const ast = lexer(tokens);
ast.forEach((node) => {
// parse...
});
}
移除冗余代码
竭尽你的全力去避免冗余代码。 冗余代码是不好的, 因为它意味着当你需要修改一些逻辑时会有多个地方需要修改。
想象一下你在经营一家餐馆, 你需要记录所有的库存西红柿, 洋葱, 大蒜, 各种香料等等。 如果你有多个记录列表, 当你用西红柿做一道菜时你得更新多个列表。 如果你只有一个列表, 就只有一个地方需要更新!
你有冗余代码通常是因为你有两个或多个稍微不同的东西, 它们共享大部分, 但是它们的不同之处迫使你使用两个或更多独立的函数来处理大部分相同的东西。 移除冗余代码意味着创建一个可以处理这些不同之处的抽象的函数/模块/类。
让这个抽象正确是关键的, 这是为什么要你遵循 Classes 那一章的 SOLID 的原因。 不好的抽象比冗余代码更差, 所以要谨慎行事。 既然已经这么说了, 如果你能够做出一个好的抽象, 才去做。 不要重复你自己, 否则你会发现当你要修改一个东西时时刻需要修改多个地方。
不好的:
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 showList(employees) {
employees.forEach((employee) => {
const expectedSalary = employee.calculateExpectedSalary();
const experience = employee.getExperience();
let portfolio = employee.getGithubLink();
if (employee.type === 'manager') {
portfolio = employee.getMBAProjects();
}
const data = {
expectedSalary,
experience,
portfolio
};
render(data);
});
}
使用 Object.assign 设置默认对象
不好的:
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',
// User did not include 'body' key
buttonText: 'Send',
cancellable: true
};
function createMenu(config) {
config = Object.assign({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
}, config);
// config now equals: {title: "Order", body: "Bar", buttonText: "Send", cancellable: true}
// ...
}
createMenu(menuConfig);
不要使用标记位做为函数参数
标记位是告诉你的用户这个函数做了不只一件事情。 函数应该只做一件事情。 如果你的函数因为一个布尔值出现不同的代码路径, 请拆分它们。
不好的:
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}`);
}
避免副作用
如果一个函数做了除接受一个值然后返回一个值或多个值之外的任何事情, 它将会产生副作用, 它可能是写入一个文件, 修改一个全局变量, 或者意外的把你所有的钱连接到一个陌生人那里。
现在在你的程序中确实偶尔需要副作用, 就像上面的代码, 你也许需要写入到一个文件, 你需要做的是集中化你要做的事情, 不要让多个函数或者类写入一个特定的文件, 用一个服务来实现它, 一个并且只有一个。
重点是避免这些常见的易犯的错误, 比如在对象之间共享状态而不使用任何结构, 使用任何地方都可以写入的可变的数据类型, 没有集中化导致副作用。 如果你能做到这些, 那么你将会比其它的码农大军更加幸福。
不好的:
// Global variable referenced by following function.
// 全局变量被下面的函数引用
// If we had another function that used this name, now it'd be an array and it
// could break it.
// 如果我们有另一个函数使用这个 name , 现在它应该是一个数组, 这可能会出现错误。
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'];
不要写入全局函数
污染全局在 JavaScript 中是一个不好的做法, 因为你可能会和另外一个类库冲突, 你的 API 的用户可能不够聪明, 直到他们得到在生产环境得到一个异常。 让我们来考虑这样一个例子: 假设你要扩展JavaScript 的 原生 Array
, 添加一个可以显示两个数组的不同之处的 diff
方法, 你可以在 Array.prototype
中写一个新的方法, 但是它可能会和尝试做相同事情的其它类库发生冲突。 如果有另外一个类库仅仅使用 diff
方法来查找数组的第一个元素和最后一个元素之间的不同之处呢? 这就是为什么使用 ES2015/ES6 的类是一个更好的做法的原因, 只要简单的扩展全局的 Array
即可。
不好的:
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));
}
}
函数式编程优于指令式编程
JavaScript 不是 Haskell 那种方式的函数式语言, 但是它有它的函数式风格。 函数式语言更加简洁并且更容易进行测试, 当你可以使用函数式编程风格时请尽情使用。
不好的:
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;
}
好的:
const programmerOutput = [
{
name: 'Uncle Bobby',
linesOfCode: 500
}, {
name: 'Suzie Q',
linesOfCode: 1500
}, {
name: 'Jimmy Gosling',
linesOfCode: 150
}, {
name: 'Gracie Hopper',
linesOfCode: 1000
}
];
const totalOutput = programmerOutput
.map((programmer) => programmer.linesOfCode)
.reduce((acc, linesOfCode) => acc + linesOfCode, 0);
封装条件语句
不好的:
if (fsm.state === 'fetching' && isEmpty(listNode)) {
// ...
}
好的:
function shouldShowSpinner(fsm, listNode) {
return fsm.state === 'fetching' && isEmpty(listNode);
}
if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
// ...
}
避免负面条件
不好的:
function isDOMNodeNotPresent(node) {
// ...
}
if (!isDOMNodeNotPresent(node)) {
// ...
}
好的:
function isDOMNodePresent(node) {
// ...
}
if (isDOMNodePresent(node)) {
// ...
}
避免条件语句
这看起来似乎是一个不可能的任务。 第一次听到这个时, 多数人会说: “没有 if
语句还能期望我干啥呢”, 答案是多数情况下你可以使用多态来完成同样的任务。 第二个问题通常是 “好了, 那么做很棒,但是我为什么想要那样做呢”, 答案是我们学到的上一条代码整洁之道的理念: 一个函数应当只做一件事情。当你有使用 if
语句的类/函数是, 你在告诉你的用户你的函数做了不止一件事情。 记住: 只做一件事情。
不好的:
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();
}
}
避免类型检查 (part 1)
JavaScript 是无类型的, 这意味着你的函数能接受任何类型的参数。 但是有时又会被这种自由咬伤,于是又尝试在你的函数中做类型检查。 有很多种方式来避免这个, 第一个要考虑的是一致的 API 。
不好的:
function travelToTexas(vehicle) {
if (vehicle instanceof Bicycle) {
vehicle.peddle(this.currentLocation, new Location('texas'));
} else if (vehicle instanceof Car) {
vehicle.drive(this.currentLocation, new Location('texas'));
}
}
好的:
function travelToTexas(vehicle) {
vehicle.move(this.currentLocation, new Location('texas'));
}
避免类型检查 (part 2)
如果你使用原始的字符串、 整数和数组, 并且你不能使用多态, 但是你依然感觉到有类型检查的需要,你应该考虑使用 TypeScript 。 它是一个常规 JavaScript 的优秀的替代品, 因为它在标准的 JavaScript语法之上为你提供静态类型。 对常规 JavaScript 做人工类型检查的问题是需要大量的冗词来仿造类型安全而不缺失可读性。 保持你的 JavaScript 简洁, 编写良好的测试, 并有良好的代码审阅, 否则使用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;
}
不要过度优化
现代化浏览器运行时在幕后做大量的优化, 在大多数的时间, 做优化就是在浪费你的时间。 这些是好的资源, 用来查看那些地方需要优化。 为这些而优化, 直到他们被修正。
不好的:
// On old browsers, each iteration with uncached `list.length` would be costly
// because of `list.length` recomputation. In modern browsers, this is optimized.
// 在旧的浏览器上, 每次循环 `list.length` 都没有被缓存, 会导致不必要的开销, 因为要重新计
// 算 `list.length` 。 在现代化浏览器上, 这个已经被优化了。
for (let i = 0, len = list.length; i < len; i++) {
// ...
}
好的:
for (let i = 0; i < list.length; i++) {
// ...
}
移除僵尸代码
僵死代码和冗余代码同样糟糕。 没有理由在代码库中保存它。 如果它不会被调用, 就删掉它。 当你需要它时, 它依然保存在版本历史记录中。
不好的:
function oldRequestModule(url) {
// ...
}
function newRequestModule(url) {
// ...
}
const req = newRequestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');
好的:
function newRequestModule(url) {
// ...
}
const req = newRequestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');