前言:在想学习一些权威的小而美的源码前,组长给我推荐了clean-code-javascript的文章,对整体的代码规范有所体会了解,之后再去看一些源码,可能会有一些不一样的感悟,于是,空闲之余,我认真看完并提取了中间感悟较深的部分,记录于此。
仓库地址:https://github.com/ryanmcdermott/clean-code-javascript
Variables(变量)
见明知意
我们读的代码将比我们写的还要多。我们编写的代码必须具有可读性
和可搜索性
,这一点很重要。如果不指定对理解程序有意义的变量,就会伤害读者。让你的名字便于搜索。像buddy.js和ESLint这样的工具可以帮助识别未命名的常量。
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);
Bad:
const address = "One Infinite Loop, Cupertino 95014";
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
saveCityZipCode(
//Cupertino
address.match(cityZipCodeRegex)[1],
//95014
address.match(cityZipCodeRegex)[2]
);
Good:
const address = "One Infinite Loop, Cupertino 95014";
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
const [_, city, zipCode] = address.match(cityZipCodeRegex) || [];
//city : Cupertino ; zipCode : 95014
saveCityZipCode(city, zipCode);
使用默认参数代替短路或条件
默认参数通常比短路更简洁。注意,如果使用它们,函数将只为undefined
提供默认值。其他“错误”的值,如' '
," "
,false
, null
, 0
和NaN
,将不会被默认值替换。
Bad:
function createMicrobrewery(name) {
const breweryName = name || "Hipster Brew Co.";
// ...
}
Good:
function createMicrobrewery(name = "Hipster Brew Co.") {
// ...
}
函数
限制函数参数(2个及以内)
限制函数参数的数量非常重要,因为它使测试函数更容易。超过三个会导致组合爆炸,你必须用每个独立的论点测试大量不同的情况。
一个或两个参数是理想情况,如果可能的话应该避免三个参数。超过这个数的都应该合并。通常,如果你有两个以上的参数,那么你的函数尝试做的太多了。在不是这样的情况下,大多数情况下,更高级别的对象作为参数就足够了。
由于JavaScript允许您动态地创建对象
,而无需大量的类样板,因此如果您发现自己需要大量的参数,则可以使用对象。
为了明确函数期望的属性,可以使用ES2015/ES6解构语法
。这有几个好处:
- 当有人查看函数签名时,马上就会清楚使用了什么属性。
- 它可以用来模拟命名参数。
- 解构还克隆传入函数的实参对象的指定原语值。这有助于防止副作用。注意:从参数对象析构的对象和数组不会被克隆。
- Linters可以警告你未使用的财产,这是不可能不破坏。
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
});
使用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);
扩展:Object.assign
该方法用于将所有可枚举属性的值从一个或多个源对象(sources)分配到目标对象(target),并返回目标对象。
Object.assign(target, ...sources)
例如
const target = { a: 1, b: 2 };
const source1 = { b: 4, c: 5 };
const source2 = { b: 6, c: 7 };
const obj = Object.assign(target,source1,source2);
console.log(obj); // {a: 1, b: 6, c: 7}
console.log(target); // {a: 1, b: 6, c: 7}
console.log(source1); // { b: 4, c: 5 }
console.log(source2); // { b: 6, c: 7 };
具体的继承属性和不可枚举属性不能拷贝,浅拷贝与深拷贝自行学习
避免写入副作用
如果函数除了接受一个值并返回另一个或多个值之外,还做了其他事情,则会产生副作用。副作用可能是写入文件,修改某些全局变量,或不小心将所有钱汇给一个陌生人。
有时候,你确实需要在一个项目中出现副作用。与前面的示例一样,您可能需要写入文件。你要做的是把你做这件事的地方集中起来。不要有几个写特定文件的函数和类。有一个服务来做这件事。一个,而且只有一个。
重点是要避免常见的陷阱,比如在没有任何结构的对象之间共享状态,使用可以被任何东西写入的可变数据类型,以及不要集中副作用发生的地方。如果你能做到这一点,你将比绝大多数其他程序员更快乐。
Bad:
//全局变量name
let name = "Ryan McDermott";
//有另一个函数使用这个名字,并且破坏它,变成了一个数组(副作用)
function splitIntoFirstAndLastName() {
name = name.split(" ");
}
splitIntoFirstAndLastName();
console.log(name); // ['Ryan', 'McDermott'];
Good:
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中,像对象和数组这种是引用变量,谨慎处理它们非常重要。JavaScript函数可以更改对象的属性或更改数组的内容,这时别的用到这个引用地址的对象数组也会随之发生变化,这很容易在其他地方导致错误。
对于这种方法有两点需要注意:
- 可能在某些情况下,您实际上想要修改输入对象,但当您采用这种编程实践时,您会发现这种情况非常罕见。大多数东西可以重构,没有副作用!
- 克隆大对象在性能方面可能会非常昂贵。幸运的是,这在实践中并不是一个大问题,因为有一些很棒的库允许这种编程方法更快,而且不像手动克隆对象和数组那样需要大量内存。
Bad:
//添加商品到购物车
const addItemToCart = (cart, item) => {
cart.push({ item, date: Date.now() });
};
Good:
const addItemToCart = (cart, item) => {
return [...cart, { item, date: Date.now() }];
};
喜欢函数式编程而不是命令式编程
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
);
封装条件
Bad:
if (fsm.state === "fetching" && isEmpty(listNode)) {
// ...
}
Good:
function shouldShowSpinner(fsm, listNode) {
return fsm.state === "fetching" && isEmpty(listNode);
}
if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
// ...
}
类型检查
JavaScript是无类型的,这意味着函数可以接受任何类型的参数。有时您会被这种自由所困扰,在函数中进行类型检查变得很有诱惑力。避免老式的instanceof
typeof
的类型检查,需要检查时拥抱TypeScript
,yyds!
对象和数据结构
使用 getters 和 setters
使用getter和setter来访问对象上的数据可能比简单地查找对象上的属性更好。你可能会问:“为什么?”好吧,下面是一份杂乱的原因清单:
- 当您想做更多的事情而不仅仅是获取对象属性时,您不必查找和更改代码库中的每个访问器。
- 使得在执行集合时添加验证变得简单。
- 封装内部表示。
- 在获取和设置时容易添加日志记录和错误处理。
- 您可以延迟加载对象的属性,比如从服务器获取它。
Bad:
function makeBankAccount() {
// ...
return {
balance: 0
// ...
};
}
const account = makeBankAccount();
account.balance = 100;
Good:
function makeBankAccount() {
//私有变量
let balance = 0;
// a "getter", 通过下面返回的对象公开
function getBalance() {
return balance;
}
// a "setter", 通过下面返回的对象公开
function setBalance(amount) {
// ... 在更新balance之前进行验证
balance = amount;
}
return {
// ...
getBalance,
setBalance
};
}
const account = makeBankAccount();
account.setBalance(100);
使对象具有私有成员
通过闭包完成
Bad:
const Employee = function(name) {
this.name = name;
};
Employee.prototype.getName = function getName() {
return this.name;
};
const employee = new Employee("John Doe");
console.log(`Employee name: {employee.getName()}`); // Employee name: John Doe
delete employee.name;
console.log(`Employee name:{employee.getName()}`); // Employee name: undefined
Good:
function makeEmployee(name) {
return {
getName() {
return name;
}
};
}
const employee = makeEmployee("John Doe");
console.log(`Employee name: {employee.getName()}`); // Employee name: John Doe
delete employee.name;
console.log(`Employee name:{employee.getName()}`); // Employee name: John Doe
使用方法链接
这种模式在JavaScript中非常有用,您可以在jQuery
和Lodash
等许多库中看到它。它允许您的代码富有表现力,而不是冗长。出于这个原因,我说,使用方法链接
,并看看您的代码会有多干净。在类函数中,只需在每个函数的末尾返回这个,就可以将更多的类方法链接到它上面(链式调用的整洁性)。
Bad:
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();
Good:
class Car {
constructor(make, model, color) {
this.make = make;
this.model = model;
this.color = color;
}
setMake(make) {
this.make = make;
// NOTE: Returning this for chaining
return this;
}
setModel(model) {
this.model = model;
// NOTE: Returning this for chaining
return this;
}
setColor(color) {
this.color = color;
// NOTE: Returning this for chaining
return this;
}
save() {
console.log(this.make, this.model, this.color);
// NOTE: Returning this for chaining
return this;
}
}
const car = new Car("Ford", "F-150", "red").setColor("pink").save();Bad:
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();
Good:
class Car {
constructor(make, model, color) {
this.make = make;
this.model = model;
this.color = color;
}
setMake(make) {
this.make = make;
// NOTE: Returning this for chaining
return this;
}
setModel(model) {
this.model = model;
// NOTE: Returning this for chaining
return this;
}
setColor(color) {
this.color = color;
// NOTE: Returning this for chaining
return this;
}
save() {
console.log(this.make, this.model, this.color);
// NOTE: Returning this for chaining
return this;
}
}
const car = new Car("Ford", "F-150", "red").setColor("pink").save();
并发
使用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);
});
但promise的链式调用并不是最简洁,ES2017/ES8带来了异步和等待,这提供了一个更干净的解决方案。你所需要的只是一个以async关键字为前缀的函数,然后你就可以在没有函数链的情况下命令式地编写逻辑。如果你今天就可以利用ES2017/ES8的特性,那么就使用它吧!
better
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()
错误处理
抛出错误是一件好事!它们意味着运行时已经成功地识别出程序中什么地方出错了,并通过停止当前堆栈上的函数执行、终止进程(在Node中)和在控制台中通过堆栈跟踪通知您来让您知道。
不要忽略捕获的错误
对捕获的错误不做任何事情并不会使您有能力修复或对该错误做出反应。将错误记录到控制台(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!
}
文章评论