26個寫出簡潔優雅JavaScript代碼的技巧
寫在前面
在編程世界中,代碼不僅僅是讓事情正常運轉。 它就像一件講述故事的藝術品。 當代碼干凈時,它就像一個美麗的、精心制作的雕塑,既美觀又運行良好。
但在急于按期完成任務的過程中,有時團隊不會太注意保持代碼的整潔。 這可能會導致項目變得混亂、復雜,變得更加難以開展。 隨著情況變得更糟,生產力也會下降。 然后,公司需要引進更多的人來提供幫助,這使得一切都變得更加昂貴。
那么,干凈的代碼是什么樣的呢? 它的代碼易于理解,沒有多余的部分,簡單,并且可以通過測試。 換句話說,它是可讀的、可重用的,并且在需要時易于更改。
為了幫助你編寫出色的 JavaScript 代碼,我將在今天的內容中與你分享 26 個寫干凈代碼的技巧,這些技巧將指導你編寫既優雅又高效的代碼。
1、變量
1.使用有意義且易于發音的變量名
// Bad
const yyyymmdstr = moment().format("YYYY/MM/DD");
// Good
const currentDate = moment().format("YYYY/MM/DD");
2. 同一類型的變量使用相同的詞匯表
// Bad
getUserInfo();
getClientData();
getCustomerRecord();
// Good
getUser();
3. 使用可搜索的名稱
我們將閱讀比我們編寫的更多的代碼,我們編寫的代碼可讀且可搜索,這一點很重要。
// Bad
// What the heck is 86400000 for?
setTimeout(blastOff, 86400000);
// Good
// Declare them as capitalized named constants.
const MILLISECONDS_PER_DAY = 60 * 60 * 24 * 1000; //86400000;
setTimeout(blastOff, MILLISECONDS_PER_DAY);
4. 使用解釋變量
// Bad
const address = "One Infinite Loop, Cupertino 95014";
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
saveCityZipCode(
address.match(cityZipCodeRegex)[1],
address.match(cityZipCodeRegex)[2]
);
// Good
const address = "One Infinite Loop, Cupertino 95014";
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
const [_, city, zipCode] = address.match(cityZipCodeRegex) || [];
saveCityZipCode(city, zipCode);
5. 避免心理映射
顯式的比隱式的好。
// Bad
const locations = ["Austin", "New York", "San Francisco"];
locations.forEach(l => {
doStuff();
doSomeOtherStuff();
// ...
// ...
// ...
// Wait, what is `l` for again?
dispatch(l);
});
// Good
const locations = ["Austin", "New York", "San Francisco"];
locations.forEach(location => {
doStuff();
doSomeOtherStuff();
// ...
// ...
// ...
dispatch(location);
});
6. 不要添加不需要的上下文
如果您的類/對象名稱告訴您一些信息,請不要在變量名稱中重復該信息。
// Bad
const Car = {
carMake: "Honda",
carModel: "Accord",
carColor: "Blue"
};
function paintCar(car, color) {
car.carColor = color;
}
// Good
const Car = {
make: "Honda",
model: "Accord",
color: "Blue"
};
function paintCar(car, color) {
car.color = color;
}
7. 使用默認參數代替短路或條件
默認參數通常比短路更清晰。 請注意,如果您使用它們,您的函數將只為未定義的參數提供默認值。 其他“假”值(例如 ''、""、false、null、0 和 NaN)不會被默認值替換。
// Bad
function createMicrobrewery(name) {
const breweryName = name || "Hipster Brew Co.";
// ...
}
// Good
function createMicrobrewery(name = "Hipster Brew Co.") {
// ...
}
2、功能
8. 函數參數(理想情況下為 2 個或更少)
限制函數參數的數量非常重要,因為它使測試函數變得更加容易。 超過三個會導致組合爆炸,您必須使用每個單獨的參數來測試大量不同的情況。
// Bad
function createMenu(title, body, buttonText, cancellable) {
// ...
}
createMenu("Foo", "Bar", "Baz", true);
//Good
function createMenu({ title, body, buttonText, cancellable }) {
// ...
}
createMenu({
title: "Foo",
body: "Bar",
buttonText: "Baz",
cancellable: true
});
9.函數應該做一件事
這是迄今為止軟件工程中最重要的規則。 當函數做不止一件事時,它們就更難編寫、測試和推理。 當您可以將一個函數隔離為一個操作時,就可以輕松重構它,并且您的代碼讀起來會更清晰。 如果您除了本指南之外沒有任何其他內容,您將領先于許多開發人員。
// 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();
}
10.函數名稱應該說明它們的作用
// Bad
function addToDate(date, month) {
// ...
}
const date = new Date();
// It's hard to tell from the function name what is added
addToDate(date, 1);
// Good
function addMonthToDate(month, date) {
// ...
}
const date = new Date();
addMonthToDate(1, date);
11.函數應該只是一層抽象
當你有多個抽象級別時,你的函數通常會做太多事情。 拆分功能可以實現可重用性和更容易的測試。
// 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 syntaxTree = 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;
}
12. 刪除重復代碼
盡最大努力避免重復代碼。 重復的代碼是不好的,因為這意味著如果您需要更改某些邏輯,則需要在多個地方進行更改。
// 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 "manager":
data.portfolio = employee.getMBAProjects();
break;
case "developer":
data.githubLink = employee.getGithubLink();
break;
}
render(data);
});
}
13. 使用Object.assign設置默認對象
// 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",
// User did not include 'body' key
buttonText: "Send",
cancellable: true
};
function createMenu(config) {
let finalConfig = Object.assign(
{
title: "Foo",
body: "Bar",
buttonText: "Baz",
cancellable: true
},
config
);
return finalConfig
// config now equals: {title: "Order", body: "Bar", buttonText: "Send", cancellable: true}
// ...
}
createMenu(menuConfig);
14. 不要使用標志作為函數參數
標志告訴你的用戶這個函數不止做一件事。 函數應該做一件事。 如果函數遵循基于布爾值的不同代碼路徑,則拆分它們。
// Bad
function createFile(name, temp) {
if (temp) {
fs.create(`./temp/${name}`);
} else {
fs.create(name);
}
}
// Good
function createFile(name) {
fs.create(name);
}
function createTempFile(name) {
createFile(`./temp/${name}`);
}
15.不要寫入全局函數
在 JavaScript 中污染全局變量是一種不好的做法,因為你可能會與另一個庫發生沖突,并且 API 的用戶在生產中遇到異常之前不會意識到這一點。
// 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));
}
}
16. 優先使用函數式編程而不是命令式編程
JavaScript 不像 Haskell 那樣是一種函數式語言,但它具有函數式風格。 函數式語言可以更簡潔、更容易測試。 盡可能喜歡這種編程風格。
// 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
}
];
const totalOutput = programmerOutput.reduce(
(totalLines, output) => totalLines + output.linesOfCode,
0
);
17.封裝條件語句
// Bad
if (fsm.state === "fetching" && isEmpty(listNode)) {
// ...
}
// Good
function shouldShowSpinner(fsm, listNode) {
return fsm.state === "fetching" && isEmpty(listNode);
}
if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
// ...
}
18.避免否定條件
// Bad
function isDOMNodeNotPresent(node) {
// ...
}
if (!isDOMNodeNotPresent(node)) {
// ...
}
// Good
function isDOMNodePresent(node) {
// ...
}
if (isDOMNodePresent(node)) {
// ...
}
3、并發性
19.使用 Promise,而不是回調
回調不干凈,并且會導致過多的嵌套。 在 ES2015/ES6 中,Promise 是內置的全局類型。 使用它們!
// Bad
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");
}
});
}
}
);
// Good
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);
});
20. Async/Await 比 Promise 更簡潔
Promise 是回調的一個非常干凈的替代方案,但 ES2017/ES8 帶來了 async 和 wait,它提供了更干凈的解決方案。
您所需要的只是一個以 async 關鍵字為前綴的函數,然后您可以命令式地編寫邏輯,而無需 then 函數鏈。
// Bad
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);
});
// Good
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()
4、錯誤處理
拋出錯誤是一件好事! 它們意味著運行時已成功識別出程序中的某些問題,并且它會通過停止當前堆棧上的函數執行、終止進程(在 Node 中)并通過堆棧跟蹤在控制臺中通知您來通知您。
21. 不要忽略捕獲的錯誤
對捕獲的錯誤不采取任何措施并不能讓您有能力修復或對所述錯誤做出反應。 將錯誤記錄到控制臺 (console.log) 也好不了多少,因為它常常會迷失在打印到控制臺的大量內容中。
如果您將任何代碼包裝在 try/catch 中,則意味著您認為那里可能會發生錯誤,因此您應該為錯誤發生時制定計劃或創建代碼路徑。
// Bad
try {
functionThatMightThrow();
} catch (error) {
console.log(error);
}
// Good
try {
functionThatMightThrow();
} catch (error) {
// One option (more noisy than console.log):
console.error(error);
// Another option:
notifyUserOfError(error);
// Another option:
reportErrorToService(error);
// OR do all three!
}
22. 不要忽視被拒絕的承諾
出于同樣的原因,您不應該忽略 try/catch 中捕獲的錯誤。
// Bad
getdata()
.then(data => {
functionThatMightThrow(data);
})
.catch(error => {
console.log(error);
});
// Good
getdata()
.then(data => {
functionThatMightThrow(data);
})
.catch(error => {
// One option (more noisy than console.log):
console.error(error);
// Another option:
notifyUserOfError(error);
// Another option:
reportErrorToService(error);
// OR do all three!
});
5、評論
23. 只評論具有業務邏輯復雜性的事物
評論是道歉,而不是要求。 好的代碼主要是文檔本身。
// Bad
function hashIt(data) {
// The hash
let hash = 0;
// Length of string
const length = data.length;
// Loop through every character in data
for (let i = 0; i < length; i++) {
// Get character code.
const char = data.charCodeAt(i);
// Make the hash
hash = (hash << 5) - hash + char;
// Convert to 32-bit integer
hash &= hash;
}
}
// Good
function hashIt(data) {
let hash = 0;
const length = data.length;
for (let i = 0; i < length; i++) {
const char = data.charCodeAt(i);
hash = (hash << 5) - hash + char;
// Convert to 32-bit integer
hash &= hash;
}
}
24. 不要在代碼庫中留下注釋掉的代碼
版本控制的存在是有原因的。 將舊代碼留在您的歷史記錄中。
// Bad
doStuff();
// doOtherStuff();
// doSomeMoreStuff();
// doSoMuchStuff();
// Good
doStuff();
25.沒有期刊評論
請記住,使用版本控制! 不需要死代碼、注釋代碼,尤其是期刊注釋。 使用 git log 獲取歷史記錄!
// Bad
/**
* 2016-12-20: Removed monads, didn't understand them (RM)
* 2016-10-01: Improved using special monads (JP)
* 2016-02-03: Removed type-checking (LI)
* 2015-03-14: Added combine with type-checking (JR)
*/
function combine(a, b) {
return a + b;
}
// Good
function combine(a, b) {
return a + b;
}
26. 避免位置標記
它們通常只是增加噪音。 讓函數和變量名稱以及正確的縮進和格式為代碼提供視覺結構。
// Bad
////////////////////////////////////////////////////////////////////////////////
// Scope Model Instantiation
////////////////////////////////////////////////////////////////////////////////
$scope.model = {
menu: "foo",
nav: "bar"
};
////////////////////////////////////////////////////////////////////////////////
// Action setup
////////////////////////////////////////////////////////////////////////////////
const actions = function() {
// ...
};
// Good
$scope.model = {
menu: "foo",
nav: "bar"
};
const actions = function() {
// ...
};
結論
從一開始就讓代碼干凈并不總是那么容易。 重要的是要記住,沒有必要執著于使每條線路都完美,尤其是當您的日程安排很緊時。 只是沒有足夠的時間來一遍又一遍地重寫代碼。
相反,專注于在有限的時間內編寫最好的代碼。 當下一輪更新到來并且您注意到可以改進的內容時,這是進行這些更改的好時機。
每個公司和每個項目都有自己的編碼風格,它可能與我分享的技巧不同。 如果您加入一個已經啟動并運行的項目,通常最好堅持使用已經存在的編碼風格(除非您要重構它)。 這是因為在整個代碼中保持一致的風格也是保持代碼整潔的一種形式。
請記住,干凈的代碼是一個旅程,而不是目的地。 這是關于隨著時間的推移做出小的改進,而不是陷入完美。 通過應用您所學到的技巧,您將能夠編寫出更簡潔、更高效的 JavaScript 代碼,未來的您和其他人都會感謝您。