全文共8912字,预计学习时长14分钟
作者lloorraa,来源need.pix
糟糕的代码可以运作,但早晚会让我们付出代价。你有没有遇到过这样的问题:几周后,你无法理解自己的代码,于是不得不花上几个小时,甚至几天的时间来弄清楚到底发生了什么。
解决这个常见问题的方法是使代码尽可能清晰。如果做得更好的话,即使是非技术人员也应该能理解你的代码。
是时候停止寻找借口,提高我们的代码质量了!
编写清晰的代码并没有那么复杂。本教程将向你展示五种改进代码的简单技巧,并提供一些实例:
1. 不用switch语句
我们通常使用switch语句来代替大型if-else-if语句。但是,switch语句非常冗长,很难维护,甚至很难调试。这些switch语句把我们的代码弄得乱七八糟,而且这些语句的语法很奇怪,很不舒服。在添加更多的case时,我们不得不必须手动添加每个case和break语句,而这就很容易出错。
接下来看一个switch语句的例子:
function getPokemon(type) {
let pokemon;
switch (type) {
case 'Water':
pokemon = 'Squirtle';
break;
case 'Fire':
pokemon = 'Charmander';
break;
case 'Plant':
pokemon = 'Bulbasur';
break;
case 'Electric':
pokemon = 'Pikachu';
break;
default:
pokemon = 'Mew';
}
return pokemon;
}
console.log(getPokemon('Fire')); // Result: Charmander
Switch语句
如果需要在switch语句中添加更多的case的话,需要编写的代码量是相当大的。我们可能最终会复制粘贴代码,而其实我们都知道这种行为的后果是什么。
那么,如何避免使用switch语句呢?可以通过使用对象文本。对象文本简单,易于编写,方便读取,维护轻松。我们都习惯用Javascript处理对象,对象文本语法比switch语句更新鲜。下面举个例子:
const pokemon = {
Water: 'Squirtle',
Fire: 'Charmander',
Plant: 'Bulbasur',
Electric: 'Pikachu'
};
function getPokemon(type) {
return pokemon[type] || 'Mew';
}
console.log(getPokemon('Fire')); // Result: Charmander
// If the type isn't found in the pokemon object, the function will return the default value 'Mew'
console.log(getPokemon('unknown')); // Result: Mew
使用对象文本替代switch
如你所见,可以使用运算符 || 添加默认值。如果在pokemon对象中找不到type,getpokemon函数将使mew返回为默认值。
小贴士:你可能已经注意到,我们在函数外部而不是内部声明pokemon对象。这样做是为了避免每次执行函数时都重新创建pokemon。
用映射也能达到同样的效果。映射就像对象一样是键-值对的集合。不同的是映射允许任何类型的键,而对象只允许字符串作为键。此外,映射还有一系列有趣的属性和方法。
以下是使用映射的方法:
const pokemon = new Map([
['Water', 'Squirtle'],
['Fire', 'Charmander'],
['Plant', 'Bulbasur'],
['Electric', 'Pikachu']
]);
function getPokemon(type) {
return pokemon.get(type) || 'Mew';
}
console.log(getPokemon('Fire')); // Result: Charmander
console.log(getPokemon('unknown')); // Result: Mew
用映射代替switch语句
如你所见,当用对象文本或映射替换switch语句时,代码看起来更清楚、更直接。
2. 把条件语句写的更有描述性
在编写代码时,条件语句是绝对必要的。然而,他们很快就会失控,最终让我们无法理解这些语句。这导致我们要么必须编写注释来解释语句的作用,要么必须花费宝贵的时间来一条一条检查代码来了解发生了什么。这很糟糕。
看一下下面的语句:
function checkGameStatus() {
if (
remaining === 0 ||
(remaining === 1 && remainingPlayers === 1) ||
remainingPlayers === 0
) {
quitGame();
}
}
复杂的条件语句
如果只看前面函数里if语句中的代码,很难理解发生了什么。代码表意不清楚,不清楚的代码只会导致技术错误,还会让人们非常头痛。
怎样改善条件语句呢?可以把它写成一个函数。以下是具体操作方法:
function isGameLost() {
return (
remaining === 0 ||
(remaining === 1 && remainingPlayers === 1) ||
remainingPlayers === 0
);
}
// Our function is now much easier to understand:
function checkGameStatus() {
if (isGameLost()) {
quitGame();
}
}
把条件语句写成函数
通过将条件提取到具有描述性名称isGameLost()的函数中,checkGameStatus函数现在就变得很容易理解。为什么?因为代码表意更清晰。它能够告诉我们发生了什么,这是我们应该一直努力的方向。
3. 用卫语句(Guard Clauses)代替嵌套的if语句
嵌套if语句是在代码中可能遇到的最可怕的事情之一。你要是想能够完全掌握代码的情况,这绝对会让你精疲力竭。下面是一个嵌套if语句的例子(这个嵌套有三层):
function writeTweet() {
const tweet = writeSomething();
if (isLoggedIn()) {
if (tweet) {
if (isTweetDoubleChecked()) {
tweetIt();
} else {
throw new Error('Dont publish without double checking your tweet');
}
} else {
throw new Error("Your tweet is empty, can't publish it");
}
} else {
throw new Error('You need to log in before tweeting');
}
}
嵌套的if语句
你可能需要花几分钟时间上下阅读,才能了解函数运作的流程。嵌套的if语句很难让人阅读和理解。那么,如何才能摆脱讨厌的嵌套if语句呢?可以反向思考,使用卫语句来替代这些句子。
“在计算机程序设计中,卫语句是一个布尔表达式,如果程序要在有问题的分支里继续运行的话,它的求值必须为true。”——维基百科
通过颠倒函数的逻辑,并在函数开始时放置导致早期退出的条件,它们将变为保护程序,并且只允许函数在满足所有条件时继续执行。这样可以避免else语句。下面是如何重构之前的函数以使用卫语句的方法:
function writeTweet() {
const tweet = writeSomething();
if (!isLoggedIn()) {
throw new Error('You need to log in before tweeting');
}
if (!tweet) {
throw new Error("Your tweet is empty, can't publish it");
}
if (!isTweetDoubleChecked()) {
throw new Error('Dont publish without double checking your tweet');
}
tweetIt();
}
用卫语句重构函数
如你所见,代码更清晰,更容易理解。我们可以简单向下阅读来了解函数的作用,遵循函数的自然流动,不像以前那样上下阅读。
4. 不要写重复的代码
写重复的代码总是以失败告终。它会导致如下情况:“我在这里修复了这个bug,但是忘记在那里修复”或“我需要在五个不同的地方更改/添加一个新功能”。
正如DRY(Don’t repeat yourself不要重复)原则所说:
每一部分知识或逻辑都必须在一个系统中有单一的、明确的表示。
因此,代码越少越好:它节省了时间和精力,更易于维护,并减少了错误的出现。
那么,如何避免重复代码呢?这有点难,但是将逻辑提取到函数/变量通常效果很好。让我们看看下面的代码,我在重构应用程序时看到了这些代码:
function getJavascriptNews() {
const allNews = getNewsFromWeb();
const news = [];
for (let i = allNews.length - 1; i >= 0; i--){
if (allNews[i].type === "Javascript") {
news.push(allNews[i]);
}
}
return news;
}
function getRustNews() {
const allNews = getNewsFromWeb();
const news = [];
for (let i = allNews.length - 1; i >= 0; i--){
if (allNews[i].type === "rust") {
news.push(allNews[i]);
}
}
return news;
}
function getGolangNews() {
const news = [];
const allNews = getNewsFromWeb();
for (let i = allNews.length - 1; i >= 0; i--) {
if (allNews[i].type === 'golang') {
news.push(allNews[i]);
}
}
return news;
}
重复代码示例
你可能已经注意到for循环在这两个函数中完全相同,除了一个小细节:我们想要的新闻类型,即Javascript或rust新闻。为了避免这种重复,可以将for循环提取到一个函数中,然后从getJavascriptNews,getRustNews和getGolangNews 函数调用该函数。以下是具体操作方法:
function getJavascriptNews() {
const allNews = getNewsFromWeb();
return getNewsContent(allNews, 'Javascript');
}
function getRustNews() {
const allNews = getNewsFromWeb();
return getNewsContent(allNews, 'rust');
}
function getGolangNews() {
const allNews = getNewsFromWeb();
return getNewsContent(allNews, 'golang');
}
function getNewsContent(newsList, type) {
const news = [];
for (let i = newsList.length - 1; i >= 0; i--) {
if (newsList[i].type === type) {
news.push(newsList[i].content);
}
}
return news;
}
在将for循环提取到getNewsContent函数中之后,getJavascriptNews, getRustNews和getGolangNews函数变成了简单、清晰的程序。
进一步重构
但是,你是否意识到,除了传递给getNewsContent的类型字符串之外,这两个函数完全相同?这是重构代码时经常发生的事情。通常情况下,更改一个会导致另一个更改,以此类推,直到重构后的代码最终变成原始代码的一半大小。代码告诉你它需要什么:
function getNews(type) {
const allNews = getNewsFromWeb();
return getNewsContent(allNews, type);
}
function getNewsContent(newsList, type) {
const news = [];
for (let i = newsList.length - 1; i >= 0; i--) {
if (newsList[i].type === type) {
news.push(newsList[i].content);
}
}
return news;
}
getJavascriptNews, getRustNews和getGolangNews函数去了哪里?将它们替换为getNews函数,该函数将新闻类型作为参数接收。这样,无论添加多少类型的新闻,总是使用相同的功能。这称为抽象,允许我们重用函数,因此非常有用。抽象是我在写代码的时候最常用的技术之一。
补充:使用es6特性使for循环更具可读性
for循环并不完全可读。通过引入es6数组函数,可以有95%的机会避免使用它们。在本例中可以使用array.filter和array.map组合来替换原始循环:
function getNews(type) {
const allNews = getNewsFromWeb();
return getNewsContent(allNews, type);
}
function getNewsContent(newsList, type) {
return newsList
.filter(newsItem => newsItem.type === type)
.map(newsItem => newsItem.content);
}
用 Array.filter 和 Array.map 来代替循环
• 用Array.filter只返回其类型等于作为参数传递的类型的元素。
• 用Array.map只返回item对象的content属性,而不是整个item。
恭喜你,经过三次简单的重构,最初的三个函数已经缩减为两个,这更容易理解和维护。另外,抽象让getNews函数变得可以重新利用。
5. 一个函数只用来做一件事
一个函数应该只做一件事。做不止一件事的函数是所有罪恶的根源,也是代码中可能遇到的最糟糕的事情之一(嵌套的if语句也是)。它们很混乱,搞得代码难以理解。下面是一个来自实际应用程序的复杂函数示例:
function startProgram() {
if (!window.indexedDB) {
window.alert("Browser not support indexeDB");
} else {
let openRequest = indexedDB.open("store