JavaScript 代码简洁之道
本文并不是代码风格指南,而是关于代码的 可读性、复用性、扩展性 探讨
文章目录
变量
用有意义且有含义的单词命名变量
Bad:
const yyyymmdstr=moment.format('YYYY/MM/DD');
Good:
const currentDate=moment().format('YYYY/MM/DD');
保持统一
当一个功能在项目中不同文件夹中出现时,此时我们应该保证该命名的统一。
Bad:
getUserInfo()
getClientData()
getCustomerRecord()
Good:
getUser()
每个变量都该命名
可以使用buddy.js或者
ESLint检测代码中未命名的常量
Bad:
setTimeout(blastOff,864000000);
Good:
const MILLISECONDS_IN_A_DAY = 86400000;
setTimeout(blastOff, MILLISECONDS_IN_A_DAY);
使用有意义的变量名
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) || [];
saveCity
直截了当
避免需要通过上下文来推断的变量,显式优于隐式
Bad:
const locations = ['Austin', 'New York', 'San Francisco'];
locations.forEach((l) => {
doStuff();
doSomeOtherStuff();
// ...
// ...
// ...
// 1 是什么?
dispatch(l);
});
Good:
const locations = ['Austin', 'New York', 'San Francisco'];
locations.forEach((location) => {
doStuff();
doSomeOtherStuff();
// ...
// ...
// ...
dispatch(location);
});
避免重复的描述
当类/对象名已经有意义时,对其变量进行命名不需要再次重复。
Bad:
var Car={
carMake:'Honda',
carModel:'Accord',
carColor:'Blue'
}
function painCar(car){
car.carColor='red'
}
Good:
var Car={
make:'Honda',
model:'Accord',
color:'Blue'
}
function paintCar(car){
car.color='Red';
}
使用默认值
Bad:
function createMicrobrewery(name) {
const breweryName = name || 'Hipster Brew Co.';
// ...
}
Good:
function createMicrobrewery(name = 'Hipster Brew Co.') {
// ...
}
函数
函数参数
理想情况下函数参数不应该超过3个,如果超过3个以上,此时我们应该通过es6解构或者将这些参数封装成一个对象
Bad:
function createMenu(title,body,buttonText,cancellable){
//....
}
Good:
function createMenu({title,body,buttonText,cancellable}){
//....
}
createMenu({
title:'Foo',
body:'Bar',
buttonText:'Baz',
cancelable:true
})
//参数封装对象形式
var menuConfig={
title:'Foo',
body:'Bar',
buttonText:'Baz',
cancelable:true
}
function createMenu(menuConfig){
//...
}
功能单一
这是软件功能中最重要的原则之一。
功能不单一的函数将导致难以重构、测试和理解。功能单一的函数易于重构,并使代码更加干净。
Bad:
function emailClients(clients){
clients.forEach(client=>{
let clientRecord=database.lookup(client);
if(clientRecord.isActive()){
email(client);
}
})
}
Good:
function emailClients(clients){
clients.forEach(client=>{
emailClientIfNeeded(client);
})
}
function emailClientIfNeeded(client){
if(isClientActive(client)){
email(client);
}
}
function isClientActive(client){
let clientRecord=database.lookup(client);
return clientRecord.isActive();
}
函数名表明其功能
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);
函数应该只做一层抽象
当函数需要的抽象多于一层时通常意味着函数功能过于复杂,需要将其进行分解以提高其可重用性和可测试性。
Bad:
function parseBatterJsAlternative(code){
const REGEXES=[
//...
];
const statements=code.split(' ');
const tokens=[];
REGEXES.forEach((REGEX)=>{
statements.forEach((statements)=>{
//...
});
});
const ast=[];
tokens.forEach((token)=>{
//let...
});
ast.forEach((node)=>{
//...
});
}
Good:
function parseBetterJSAlternative(code) {
const tokens = tokenize(code);
const ast = lexer(tokens);
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;
}
删除重复代码
很多时候在一个函数中由于一两点的不同,让我们去重新写一个函数。
要想优化重复代码需要有较强的抽象能力,错误的抽象还不如重复代码。所以在抽象过程中必须要遵循 SOLID 原则
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);
});
}
使用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',
//body key 缺失
buttonText:'Send',
cancellable:true
};
unction createMenu(config){
config=Object.assign({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
},config);
//config 变成:{title:'Order',body:'Bar',buttonText:'Send',cancelable:true}
};
createMenu(menuConfig);
不要用flag作为参数
通过 flag的true或false,来判读执行逻辑,违反了一个函数干一件事的原则,因此要拆分该函数。
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}`);
}
避免副作用(第一部分)
函数接受一个值返回一个新值,除此之外的行为我们都称为副作用,比如修改全局变量、对文件进行IO操作等。
当函数确定需要副作用时,请不要用多个函数/类进行文件操作,有且仅用一个函数/类来处理。
副作用的三大天坑:随意修改可变数据类型、随意分享没有数据结构的状态、没有在统一地方处理副作用。
Bad:
// 全局变量被一个函数引用
// 现在这个变量从字符串变成了数组,如果有其他的函数引用,会发生无法预见的错误。
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中,基本类型通过赋值传递,对象和数组通过引用传递.以引用传递为例:
假如我们写一个购物车,通过 addItemToCart()
方法添加商品到购物车,修改cart
数组。此时调用purchase()
方法购买,由于引用传递,获取的购物车数组 正好是最新的数据。
那么我们来看一种坏情况:
用户点击购买时,该按钮调用purchase()
功能产生网络请求并将cart
数组数据发送到服务器。因为糟糕的网络连接,purchase()
功能必须继续重试请求。现在用户又继续添加新的商品,这时网络恢复,那么purchase()
方法获取到的cart
数组就是错误的。
为了避免这种问题,我们需要每次新增商品时,克隆cart
数组并返回新的数组。
Bad:
const addItemToCart = (cart, item) => {
cart.push({ item, date: Date.now() });
};
Good:
const addItemToCart = (cart, item) => {
return [...cart, { item, date: Date.now() }];
};
不要全局函数
在 JS 中污染全局是一个非常不好的实践,这么做可能和其他库起冲突。
想象以下例子:如果你想扩展 JS 中的Array
,为其添加一个 diff
函数显示两个数组间的差异,此时应如何去做?你可以将diff
写入 Array.prototype
,但这么做会和其他有类似需求的库造成冲突。如果另一个库对 diff
的需求为比较一个数组中首尾元素间的差异呢?
使用 ES6 中的 class 对全局的 Array 做简单的扩展显然是一个更棒的选择。
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));
}
}
采用函数式编程
函数式的编程具有更干净且便于测试的特点,尽可能使用这种风格。
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
.map(output => output.linesOfCode)
.reduce((totalLines, lines) => totalLines + lines,0);
封装条件语句
Bad:
if (fsm.state === 'fetching' && isEmpty(listNode)) {
// ...
}
Good:
function shouldShowSpinner(fsm, listNode) {
return fsm.state === 'fetching' && isEmpty(listNode);
}
if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
// ...
}
尽量别"否定情况"
Bad:
function isDOMNodeNotPresent(node) {
// ...
}
if (!isDOMNodeNotPresent(node)) {
// ...
}
Good:
function isDOMNodePresent(node) {
// ...
}
if (isDOMNodePresent(node)) {
// ...
}
避免条件判断
这看起来似乎不太可能。
大多人听到这的第一反应是:“怎么可能不用 if 完成其他功能呢?”许多情况下通过使用多态(polymorphism)可以达到同样的目的。
第二个问题在于采用这种方式的原因是什么。答案是我们之前提到过的:保持函数功能的单一性。
Bad:
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();
}
}
}
Good:
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();
}
}
避免类型判断(第一部分)
JS 是弱类型语言,这意味着函数可接受任意类型的参数。
有时候会给我们带来麻烦,我们就会用类型判读类避免这些情况发生。仔细想想是你真的需要检查类型还是你的 API 设计有问题?
Bad:
function travelToTexas(vehicle) {
if (vehicle instanceof Bicycle) {
vehicle.pedal(this.currentLocation, new Location('texas'));
} else if (vehicle instanceof Car) {
vehicle.drive(this.currentLocation, new Location('texas'));
}
}
Good:
function travelToTexas(vehicle) {
vehicle.move(this.currentLocation, new Location('texas'));
}
避免类型判读(第二部分)
如果你需要做静态类型检查,比如字符串、整数等,推荐使用 TypeScript,不然你的代码会变得又臭又长。
Bad:
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');
}
Good:
function combine(val1, val2) {
return val1 + val2;
}
避免过度优化
现代的浏览器在运行时会对代码自动进行优化。有时人为对代码进行优化可能是在浪费时间。这里可以找到许多真正需要优化的地方点击这里
Bad:
// 这里使用变量len是因为在老式浏览器中,
// 直接使用正例中的方式会导致每次循环均重复计算list.length的值,
// 现代浏览器已对此做了优化。
for (let i = 0, len = list.length; i < len; i++) {
// ...
}
Good:
for (let i = 0; i < list.length; i++) {
// ...
}
删除无效代码
不再被调用的代码应及时删除。
Bad:
function oldRequestModule(url) {
// ...
}
function newRequestModule(url) {
// ...
}
const req = newRequestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');
Good:
function newRequestModule(url) {
// ...
}
const req = newRequestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');
完成进度
- 变量
- 函数
- 对象和数据结构
- 类
- SOLD
- 测试
- 异步
- 错误处理
- 代码风格
- 注释
参考如下:
ryanmcdermott的 《clearn-code-javascript》
alivebao 翻译的https://github.com/alivebao/clean-code-js
书写该文章只是为了自己对知识点的梳理,本文都是通过mardown自己手打,如有错误请指正