复制 普通人读的懂的代码, 才是好代码 --- 沃滋基梭德
概述篇
代码可读性 代码必须是对人类是可读的。不要考虑计算机如何处理它,因为会有许多工具来转换我们的代码(编译器)。因此,最重要的是,代码将是人类可读的,因为你在开发代码时,最长的工作就是阅读代码,而不是写代码。
复制 const users = [{ id: 1, name: "Carlos Caballero", memberSince: "1997–04–20", favoriteLanguageProgramming: ["JavaScript", "C", "Java"] }, { id: 2, name: "Antonio Villena", memberSince: "2014–08–15", favoriteLanguageProgramming: ["Go", "Python", "JavaScript"] }, { id: 3, name: "Jesús Segado", memberSice: "2015–03–15", favoriteLanguageProgramming: ["PHP", "JAVA", "JavaScript"] } ];
/***********************/
const users = [
{ id: 1, name: "Carlos Caballero", memberSince: "1997–04–20", favoriteLanguageProgramming: ["JavaScript", "C", "Java"] },
{ id: 2, name: "Antonio Villena", memberSince: "2014–08–15", favoriteLanguageProgramming: ["Go", "Python", "JavaScript"] },
{ id: 3, name: "Jesús Segado", memberSice: "2015–03–15", favoriteLanguageProgramming: ["PHP", "JAVA", "JavaScript"] },
];
/***********************/
const users = [{
id: 1,
name: "Carlos Caballero",
memberSince: "1997–04–20",
favoriteLanguageProgramming: ["JavaScript", "C", "Java"],
},
{
id: 2,
name: "Antonio Villena",
memberSince: "2014–08–15",
favoriteLanguageProgramming: ["Go", "Python", "JavaScript"],
},
{
id: 3,
name: "Jesús Segado",
memberSince: "2015–03–15",
favoriteLanguageProgramming: ["PHP", "JAVA", "JavaScript"],
}];
复制 const benutzer = {
id: 1,
name: "John Smith",
mitgliedVon: "1997–04–20",
};
Gehaltserhöhung(benutzer, 1000);
/***********************/
const użytkownik = {
id: 1,
imię: "John Smith",
członekZ: "1997–04–20",
};
wzrostWynagrodzeń(użytkownik, 1000);
/***********************/
const user = {
id: 1,
name: "John Smith",
memberSince: "1997–04–20",
};
increaseSalary(user, 1000);
团队协作 可以通过配置一个标准的 .editorconfig 文件
复制 root = true
[*]
end_of_line = lf
insert_final_newline = true
[*.{js,py}]
charset = utf-8
[*.py]
indent_style = space
indent_size = 4
[Makefile]
indent_style = tab
[*.js]
indent_style = space
indent_size = 2
[{package.json,.travis.yml}]
indent_style = space
indent_size = 2
有一个在业界广泛使用的工具被称为 Prettier,它能够根据 linter 的规则实时改变我们的代码格式(IDE的插件)。
变量篇
变量名要名副其实 变量的名称必须能够描述出该变量的作用和用途。
在选择变量的名称时,另一个错误的行为是删除一个词中某些字母,使用这些缩略语读起来很困难。首先,我们是在用英语编码,而且不是所有的开发者都是讲英语的。因此,我们为什么要把它的名字减少 3 或 4 个字符?这有什么好处呢?代码会被工具(转译器包括其他语言的编译器)操作,最终正确地完成格式化(使用 Prettier)。因此,把不能发音的名字放在一起,只能让我们更费力地去推断变量的用途。
不要在名称中使用变量的类型 在变量名称中使用数据类型作为前缀是一个很古老的做法,现在让我们反思一下这个问题。
变量名称中必须要用类型作为前缀吗? 每个公司和工程都有各自的前缀规范,那如何去学习和书写这种代码呢? 如果我在变量的名称中使用一种编程语言的类型系统,为什么要用它呢? 当变量的数据类型发生变化时,比如把 Array 修改为 Map,这种情况怎么处理? 这个前缀能给我们带来什么?它是可以发音的吗?
复制 const aCountries = [];
const sName = "";
const dAmount = 3.2;
const countries = [];
const name = "";
const amount = 3.2;
对同一类型的变量使用相同的词汇表 对同一类型的数据使用相同的词汇表。如果我们需要检索一个用户或客户的信息,我们不能以不同的方式称呼用户或客户。也就是说,不能有时称其为 user,有时称其为 customer,甚至是 client 这个词。更不可 取的是,在变量名称上额外再加一个后缀。 下面代码就是很好的示例,同一个概念,有三个不同的定义。必须自始至终使用统一的命名,不管是 user、customer 还是 client,只能用同一个。
复制 getUserInfo();
getClientData();
getCustomerRecord();
getUser();
不要添加不需要的上下文 在变量名称中没有必要添加类或包的相关上下文。 在变量名称中添加上下文是很常见的,这样可以知道这个变量位于哪个工作区。
复制 const Car = {
carMake: 'Honda',
carModel: 'Accord',
carColor: 'Blue',
}
function paintCar(car){
car.carColor = 'red'
}
const Car = {
make: 'Honda',
model: 'Accord',
color: 'Blue'
}
function paint(car){
car.color = 'Red'
}
不要使用魔法数字和字符串 在编写代码时,不应该在源代码中直接使用数字或文本字符串,这些通常也被称为魔法数字.这个数字是什么意思?必须要解释这个数字吗?这让我们不得不思考业务逻辑之外的事情。
这些魔法数字或字符串必须存储在常量中,通过对常量的名称来表达出它们的用途。在业务逻辑层面上,对于那些有意义的数字或文本字符串,如果没有一个确切的名字就会引起噪音。
在业务逻辑层面上,对于那些有意义的数字或文本字符串,如果没有一个确切的名字就会引起噪音。
复制 // what the heck is 86400000 for?
setTimeout(blastOff, 864000000);
user.rol = 'Administrator';
const MILLISECONDS_IN_A_DAY = 'Administrator';
setTimeout(blastOff, MILLISECONDS_IN_A_DAY);
user.rol = ADMINISTRATOR_ROL
复杂判断
不要使用标记 (flag) 作为函数参数 这样会破坏函数功能的单一性,我们必须创建两个函数来实现各自对应的逻辑功能,而不是使用一个函数来实现两个逻辑功能,因为他们是不同的功能。
复制 // Dirty
function book(customer, isPremium) {
// ...
if (isPremium) {
premiumLogic();
} else {
regularLogic();
}
}
// Clean (Declarative way)
function bookPremium (customer) {
premiumLogic();
}
function bookRegular (customer) {
retularLogic();
}
复制 if (platform.state === 'fetching' && isEmpty(cart)) {
// ...
}
function showLoading(platform, cart) {
return platform.state === 'fetching' && isEmpty(cart);
}
if (showLoading(platform, cart)) {
// ...
}
复制 function getPayAmount() {
let result;
if (isDead){
result = deadAmount();
} else {
if (isSeparated){
result = separatedAmount();
} else {
if (isRetired){
result = retiredAmount();
}else{
result = normalPayAmount();
}
}
}
return result;
}
function getPayAmount() {
if (isDead) return deadAmount();
if (isSeparated) return separatedAmount();
if (isRetired) return retiredAmount();
return normalPayAmount();
}
空对象 不断检查对象是否为null, 并根据该检查判断是否显示默认操作。这种模式称为空对象模式,我们默认封装了空对象行为的对象
复制 class Dog {
sound() {
return 'bark';
}
}
['dog', null].map((animal) => {
if(animal !== null) {
sound();
}
});
class Dog {
sound() {
return 'bark';
}
}
class NullAnimal {
sound() {
return null;
}
}
function getAnimal(type) {
return type === 'dog' ? new Dog() : new NullAnimal();
}
['dog', null].map((animal) => getAnimal(animal).sound());
// Returns ["bark", null]
使用多态删除条件 通过判断对象的类型去定义对象的方法,但是这里的条件语句被滥用了,在这种场景下,我们可以通过类的继承,为每个特定类型创建一个类,利用多态来避免使用条件判断。
复制 function Auto() {
}
Auto.prototype.getProperty = function () {
switch (type) {
case BIKE:
return getBaseProperty();
case CAR:
return getBaseProperty() - getLoadFactor();
case BUS:
return (isNailed) ?
0 :
getBaseProperty(voltage);
}
throw new Exception("Should be unreachable");
};
abstract class Auto {
abstract getProperty();
}
class Bike extends Auto {
getProperty() {
return getBaseProperty();
}
}
class Car extends Auto {
getProperty() {
return getBaseProperty() - getLoadFactor();
}
}
class Bus extends Auto {
getProperty() {
return (isNailed) ?
0 :
getBaseProperty(voltage);
}
}
// Somewhere in client code
speed = auto.getProperty();
函数
使用默认参数去代替短路操作或条件赋值
在大多数编程语言中,函数的参数支持设置默认值。这就意味着我们可以在代码中避免使用短路操作和条件赋值。
复制 function setName(name) {
const newName = name || 'Juan Palomo';
}
function setName(name = 'Juan Palomo') {
// ...
}
当一个函数有很多参数时,可以把这些参数组合在一起构成一个对象。我们需要避免使用多个基础类型 (如字符串、数字、布尔值等) 作为函数的入参,而是要使用抽象级别更高的对象作为入参。这样我们会更接近业务逻辑,并且更加远离底层实现。
复制 function newBurger(name, price, ingredients, vegan) {
// ...
}
function newBurger(burger) {
// ...
}
function newBurger({ name, price, ingredients, vegan }) {
// ...
}
const burger = {
name: 'Chicken',
price: 1.25,
ingredients: ['chicken'],
vegan: false,
};
newBurger(burger);
第一个例子中,我们实现了一个生产汉堡的函数,它有 4 个参数。这些参数是固定的,并且必须按照这个顺序传参,这会给我们带来很多的限制。这样的函数在使用的时候不是很灵活。
第二个例子中,最大的改进就是使用一个对象来作为参数,只需要传入一个 burger 对象就可以生产出一个 “新汉堡”。通过这种方式,我们将汉堡的基本属性整合到 1 个对象里面。
在第三个例子中,我们对传入的对象进行解构赋值,让对象的属性在函数体中可访问到,但是实际上我们传入的仅仅是一个参数,这使得这个函数有了更大的灵活性。
避免副作用 - 全局变量 副作用是未来麻烦的根源。虽然从定义上来说副作用不一定是有害的,但是如果在项目中没有节制的引起副作用,代码出错的可能性就会大大提高。 不惜一切代价避免副作用,并且确保函数是可以被测试到的。
复制 let fruits = 'Banana Apple';
function splitFruits() {
fruits = fruits.split(' ');
}
splitFruits();
console.log(fruits); // ['Banana', 'Apple'];
改进后
复制 function splitFruits(fruits) {
return fruits.split(' ');
}
const fruits = 'Banana Apple';
const newFruits = splitFruits(fruits);
console.log(fruits); // 'Banana Apple';
console.log(newFruits); // ['Banana', 'Apple'];
避免副作用 - 可变对象 是直接修改对象本身,如果你一直从事计算机相关的工作,你会知道 JavaScript 自诞生以来就是支持对象可变的,目前许多库都在尽量避免使用可变对象。
数组的方法一般被分为两部分: 一部分是会对数组本身进行修改的方法,例如: push、pop、sort,另一部分是不会对数组本身产生修改的方法例如:filter、reduce 、map 等。
复制 const addItemToCart = (cart, item) => {
cart.push({ item, date: Date.now() });
};
//无副作用
const addItemToCart = (cart, item) => {
return [...cart, {
item,
date: Date.now(),
}];
};
复制 function emailCustomers(customers) {
customers.forEach((customer) => {
const customerRecord = database.find(customer);
if (customerRecord.isActive()) {
email(client);
}
});
}
function emailActiveCustomers(customers) {
customers
.filter(isActiveCustomer)
.forEach(email);
}
function isActiveCustomer(customer) {
const customerRecord = database.find(customer);
return customerRecord.isActive();
}
函数应该只是有一个抽象级别 每个函数应仅具有单个抽象级别。
复制 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...
});
}
const REGEXES = [ // ...];
function tokenize(code) {
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...);
}
优先考虑函数式编程而不是命令式编程 函数式编程相对于命令式编程相比的另一个特点是代码更具可读性。 函数式编程在这方面就具有很大的优势;但对于那些使用命令式编程学习并开始解决问题的初级程序员来说,他们很难使用这种编程范式,因为它改变了他们的工作习惯。但是在这个行业中,我们必须适应变化,况且目前使用函数式编程的场景越来越多。
复制 const items = [{
name: 'Coffe',
price: 500
}, {
name: 'Ham',
price: 1500
}, {
name: 'Bread',
price: 150
}, {
name: 'Donuts',
price: 1000
}
];
let total = 0;
for (let i = 0; i < items.length; i++) {
total += items[i].price;
}
// 优化后
const total = items
.map(({ price }) => price)
.reduce((total, price) => total + price);
复制 class Car {
constructor({ make, model, color } = car) {
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({make: 'hoel', model: 'Jetta', color: 'gray'});
car.setColor('red');
car.save();
复制 class Car {
constructor({ make, model, color } = car){
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;
}
}
const car = new Car({make: 'hoel', model: 'Jetta', color: 'gray'});
.setColor('red')
.save();
重构篇
重构很重要的原因:
为用户提供更好的一致性
重构不是银弹,但它是一种有价值的武器,可以帮助你控制好代码和项目 (软件/应用)。它是一个科学的过程,对现有的代码进行改造,使代码可读性更高、更好理解和更整洁。而且,使添加新功能、构建大型应用程序以及发现和修复 bug 变得非常便捷。
撒密码是由明文和密文两个字母表构成的,密文字母表是将明文字母表向左或向右移动一个固定位置得到的。例如,这个恺撒密码,使用的偏移量为 6,相当于右移 6 位
复制 Plain:
ABCDEFGHIJKLMNOPQRSTUVWXYZ
Cipher:
GHIJKLMNOPQRSTUVWXYZABCDEF
重构建议: 无论我们如何重构代码,都要使用自动化测试来帮助我们验证有没有 “破坏” 代码。
复制 console.assert(cipher
('Hello World',1) === 'Ifmmp!Xpsme',
`${cipher('Hello World', 1)} === 'Ifmmp!Xpsme'`,
);
console.assert(decipher(cipher
('Hello World',3),3) === 'Hello World',
`${decipher(cipher('Hello World', 3), 3)} === 'Hello World'`,);
魔法数字 通过定义语义化的变量来移除代码中出现的魔法数字
复制 const NUMBER_LETTERS = 26;
const LETTER ={
a: 65,
z: 90,
A: 97,
Z: 122
}
从 if-else 中提取相似代码 下一步是将代码中重复的代码提取到函数中。具体来说,if 控制结构体中的赋值代码在整个代码中是重复的,我们可以提取这些赋值代码。
避免使用 else 下一步是避免使用 else 控制结构。避免使用它是很容易的,我们只需要在循环开始之前将代码从 else 移到变量 character,并作为它的默认值。