首先:需要一个好的命名
推荐:https://unbug.github.io/codelf/(外网) 进行查询命名。http://codeif.xinke.org.cn/这个国内的访问不了,过一段时间应该就可以了~
1、函数只做一件事
函数做了较多的事情,它就难以组合、测试和推测。同时让函数只做一件事情的时候,它们就很容易重构。
/**
* 坏示例
*/
function showStudent(ssn) {
const student = db.get(ssn);
if (student !== null) {
document.querySelector(`#${elementId}`).innerHTML =`${student.ssn},${student.firstName},${student.lastName}`
} else {
thrownewError("student not found")
}
}
showStudent("444-44-4444")
/**
* 好示例
*/
function findStudent(db, id) {
const student = db.get(id);
if (student === null) {
thrownewError("student not found");
}
};
function getStudentInfo(student) {
return `${student.ssn},${student.firstName},${student.lastName}`
};
function showStudentInfo(elementId, info) {
document.querySelector(elementId).innerHTML = info;
}
function showStudent(ssn) {
const student = findStudent(ssn);
let studentInfo = getStudentInfo(student);
showStudentInfo(elementId, studentInfo);
}
//只是做了些许的改进,但已开始展现出很多的优势:undefined
2、每个函数一个抽象层级
函数中混杂不同的抽象层级,往往让人迷惑。读者可能无法判断某个表达式是基础概念还是细节。更恶劣的是,就像破损的窗户,一旦细节和基础概念混杂,更多的细节就会在函数中纠结起来。理解抽象层次请参考:抽象层次
// Bad
function parseBetterJSAlternative(code) {
let REGEXES = [
// ...
];
let statements = code.split(" ");
let tokens;
REGEXES.forEach((REGEX) => {
statements.forEach((statement) => {
// ...
})
});
let ast;
tokens.forEach((token) => {
// lex...
});
ast.forEach((node) => {
// parse...
})
}
// Good
function tokenize(code) {
let REGEXES = [
// ...
];
let statements = code.split(" ");
let tokens;
REGEXES.forEach((REGEX) => {
statements.forEach((statement) => {
// ...
})
});
return tokens;
}
function lexer(tokens) {
let ast;
tokens.forEach((token) => {
// lex...
});
return ast;
}
function parseBetterJSAlternative(code) {
let tokens = tokenize(code);
let ast = lexer(tokens);
ast.forEach((node) => {
// parse...
})
}
3、使用描述性的名称
函数越短小,功能越集中,就越便于取个好名字。长而具有描述性的名称,要比短而令人费解的名称好。长而具有描述性的名称,要比描述性的长注释好。使用某种命名约定,让函数名称中的多个单词容易阅读,然后使用这些单词给函数取个能说明其功能的名称。
// Bad
function dateAdd(date, month) {
// ...
}
let date = newDate();
// 很难从函数名了解到加了什么
dateAdd(date, 1);
function write(name){
// ...
}
function assertEqual(a,b){
// ...
}
// Good
function dateAddMonth(date, month) {
// ...
}
let date = newDate();
dateAddMonth(date, 1);
// 告诉我们 name 是一个 field
function writeField(name){
// ...
}
// 能更好的解释参数的顺序和意图
function assertExpectedEqualActual(expected,actual){
// ...
}
4、函数参数
最理想的参数数量是零,其次是一(单参数函数),再次是二(双参数函数),应尽量避免三(三参数函数)。有足够的理由才能使用三个以上参数。如果函数需要三个以上参数,就说明其中一些参数应该放在一个对象中了。参数越多,函数越不容易理解,同时编写能确保参数的各种组合运行正常的测试用例越困难。
5、避免副作用
如果一个函数不是获取一个输入的值并返回其它值,它就有可能产生副作用。这些副作用可能是写入文件、修改一些全局变量、屏幕打印或者日志记录、查询HTML文档、浏览器的cookie或访问数据库。无论哪种情况,都具有破坏性,会导致古怪的时序性耦合及顺序依赖。现在你确实需要在程序中有副作用。像前面提到的那样,你可能需要写入文件。现在你需要做的事情是搞清楚在哪里集中完成这件事情。不要使用几个函数或类来完成写入某个特定文件的工作。采用一个,就一个服务来完成。
// Bad
// 下面的函数使用了全局变量。
// 如果有另一个函数在使用 name,现在可能会因为 name 变成了数组而不能正常运行。
var name = "Ryan McDermott";
function splitIntoFirstAndLastName() {
name = name.split(" ");
}
splitIntoFirstAndLastName();
console.log(name); // ["Ryan", "McDermott"];
// Good
function splitIntoFirstAndLastName(name) {
return name.split(" ");
}
var name = "Ryan McDermott"
var newName = splitIntoFirstAndLastName(name);
console.log(name); // "Ryan McDermott";
console.log(newName); // ["Ryan", "McDermott"];
6、删除重复代码
重复代码意味着你要修改某些逻辑的时候要修改不止一个地方的代码。
// Bad
function showDeveloperList(developers) {
developers.forEach(developers => {
var expectedSalary = developer.calculateExpectedSalary();
var experience = developer.getExperience();
var githubLink = developer.getGithubLink();
var data = {
expectedSalary: expectedSalary,
experience: experience,
githubLink: githubLink
};
render(data);
});
}
function showManagerList(managers) {
managers.forEach(manager => {
var expectedSalary = manager.calculateExpectedSalary();
var experience = manager.getExperience();
var portfolio = manager.getMBAProjects();
var data = {
expectedSalary: expectedSalary,
experience: experience,
portfolio: portfolio
};
render(data);
});
}
// Good
function showList(employees) {
employees.forEach(employee => {
var expectedSalary = employee.calculateExpectedSalary();
var experience = employee.getExperience();
var portfolio;
if (employee.type === "manager") {
portfolio = employee.getMBAProjects();
} else {
portfolio = employee.getGithubLink();
}
var data = {
expectedSalary: expectedSalary,
experience: experience,
portfolio: portfolio
};
render(data);
});
}
7、使用更优雅写法
1、使用默认参数代替短路表达式
// Bad
function writeForumComment(subject, body) {
subject = subject || "No Subject";
body = body || "No text";
}
// Good
function writeForumComment(subject = "No subject", body = "No text") {
...
}
2、用 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) {
config = Object.assign({
title: "Foo",
body: "Bar",
buttonText: "Baz",
cancellable: true
}, config);
// 现在 config 等于: {title: "Foo", body: "Bar", buttonText: "Baz", cancellable: true}
// ...
}
createMenu(menuConfig);
3、喜欢上命令式编程之上的函数式编程
// Bad
const programmerOutput = [
{
name: "Uncle Bobby",
linesOfCode: 500
}, {
name: "Suzie Q",
linesOfCode: 1500
}, {
name: "Jimmy Gosling",
linesOfCode: 150
}, {
name: "Gracie Hopper",
linesOfCode: 1000
}
];
var totalOutput = 0;
for (var 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
}
];
var totalOutput = programmerOutput
.map((programmer) => programmer.linesOfCode)
.reduce((acc, linesOfCode) => acc + linesOfCode, 0);
4. 以HashMap取代条件表达式
// bad
let getSpeed = type => {
switch (type) {
case SPEED_TYPE.AIR:
return getAirSpeed()
case SPEED_TYPE.WATER:
return getWaterSpeed()
...
}
}
// good
let speedMap = {
[SPEED_TYPE.AIR]: getAirSpeed,
[SPEED_TYPE.WATER]: getWaterSpeed
}
let getSpeed = type => speedMap[type] && speedMap[type]()
8、不要把标记用作函数参数
//标记告诉你的用户这个函数做的事情不止一件。但是函数应该只做一件事。如果你的函数中会根据某个布尔参数产生不同的分支,那就拆分这个函数。
// Bad
function createFile(name, temp) {
if(temp) {
fs.create("./temp/"+ name);
} else{
fs.create(name);
}
}
// Good
function createTempFile(name) {
fs.create("./temp/"+ name);
}
function createFile(name) {
fs.create(name);
}
9、对函数中条件处理
1、封装条件
// Bad
if(fsm.state === "fetching"&& isEmpty(listNode)) {
/// ...
}
// Good
function shouldShowSpinner(fsm, listNode) {
return fsm.state === "fetching"&& isEmpty(listNode);
}
if(shouldShowSpinner(fsmInstance, listNodeInstance)) {
// ...
}
2、避免否定条件
// Bad
function isDOMNodeNotPresent(node) {
// ...
}
if(!isDOMNodeNotPresent(node)) {
// ...
}
// Good
function isDOMNodePresent(node) {
// ...
}
if(isDOMNodePresent(node)) {
// ...
}
10、提前让函数退出代替嵌套条件分支
许多程序员都有这样一种观念:“每个函数只能有一个入口和一个出口。”现代编程语言都会限制函数只有一个入口。但关于“函数只有一个出口”,往往会有一些不同的看法。
下面这段伪代码是遵守“函数只有一个出口的”的典型代码:
var del = function (obj) {
var _ret;
if (!obj.isReadOnly) {
// 不为只读的才能被删除
if (obj.isFolder) {
// 如果是文件夹
_ret = deleteFolder(obj);
} else if (obj.isFile) { // 如果是文件
_ret = deleteFile(obj);
}
} return _ret;
};
嵌套的条件分支往往是由一些深信“每个函数只能有一个出口的”程序员写出的。但实际上,如果对函数的剩余部分不感兴趣,那就应该立即退出。引导阅读者去看一些没有用的else片段,只会妨碍他们对程序的理解。
于是我们可以挑选一些条件分支,在进入这些条件分支之后,就立即让这个函数退出。即在面对一个嵌套的if分支时,我们可以把外层if表达式进行反转。重构后的del函数如下:
var del = function (obj) {
if (obj.isReadOnly) { // 反转if表达式
return;
}
if (obj.isFolder) {
return deleteFolder(obj);
}
if (obj.isFile) {
return deleteFile(obj);
}
};
11、 少用三目运算符
有一些程序员喜欢大规模地使用三目运算符,来代替传统的if、else。理由是三目运算符性能高,代码量少。不过,这两个理由其实都很难站得住脚。
即使我们假设三目运算符的效率真的比if、else高,这点差距也是完全可以忽略不计的。在实际的开发中,即使把一段代码循环一百万次,使用三目运算符和使用if、else的时间开销处在同一个级别里。
同样,相比损失的代码可读性和可维护性,三目运算符节省的代码量也可以忽略不计。让JS文件加载更快的办法有很多种,如压缩、缓存、使用CDN和分域名等。把注意力只放在使用三目运算符节省的字符数量上,无异于一个300斤重的人把超重的原因归罪于头皮屑。
如果条件分支逻辑简单且清晰,这无碍我们使用三目运算符:
var global = typeof window !== "undefined" ? window : this;
12、用return退出多重循环
假设在函数体内有一个两重循环语句,我们需要在内层循环中判断,当达到某个临界条件时退出外层的循环。我们大多数时候会引入一个控制标记变量:
var func = function () {
var flag = false;
for (var i = 0; i < 10; i++) {
for (var j = 0; j < 10; j++) {
if (i * j > 30) {
flag = true; break;
}
}
if (flag === true) {
break;
}
}
};
第二种做法是设置循环标记:
var func = function () {
outerloop:
for (var i = 0; i < 10; i++) {
innerloop:
for (var j = 0; j < 10; j++) {
if (i * j > 30) {
break outerloop;
}
}
}
};
这两种做法无疑都让人头晕目眩,更简单的做法是在需要中止循环的时候直接退出整个方法:
var func = function () {
for (var i = 0; i < 10; i++) {
for (var j = 0; j < 10; j++) {
if (i * j > 30) {
return;
}
}
}
};
当然用return直接退出方法会带来一个问题,如果在循环之后还有一些将被执行的代码呢?如果我们提前退出了整个方法,这些代码就得不到被执行的机会:
var func = function () {
for (var i = 0; i < 10; i++) {
for (var j = 0; j < 10; j++) {
if (i * j > 30) {
return;
}
}
}
console.log(i); // 这句代码没有机会被执行
};
为了解决这个问题,我们可以把循环后面的代码放到return后面,如果代码比较多,就应该把它们提炼成一个单独的函数:
var print = function (i) {
console.log(i);
};
var func = function () {
for (var i = 0; i < 10; i++) {
for (var j = 0; j < 10; j++) {
if (i * j > 30) {
return print(i);
}
}
}
};
func();