前言

本篇文章讲述如何编写高效美丽的JavaScript代码,让你在开发中有更多的选择而不是停留在基础编码环节。

纯函数

纯函数指的是在给定相同输入的情况下始终返回相同输出的函数。除了他提供的输入以外,他不依赖于任何外部变量,也不更改任何外部变量。拥有纯函数使得测试更加容易,因为可以随时模拟并测试输入的值,如下:

1
2
3
4
function plusAbc(a, b, c{  // 这个函数的输出将变化无常,因为api返回的值一旦改变,同样输入函数的a,b,c的值,但函数返回的结果却不一定相同。
  var c = fetch('../api');
  return a+b+c;
}

虽然上面的代码看起来很合理,但是该函数本身依赖一个外部变量,所以如果这个变量他被修改,就会有不同的输出,在我们排查错误的时候就比较不方便。如果写了纯函数,确保他没有引入和修改外部的变量,那么就能降低错误的发生。下面是经过改造后的代码

1
2
3
function plusAbc(a, b, c) {  // 同样输入函数的a,b,c的值,但函数返回的结果永远相同。
return a+b+c;
}

参数传递与解析

使用函数的时候,如果我们需要大量的传入参数,那么这会在调用的时候变成一件非常可怕的事情

1
2
3
4
5
const createButton = (title, color, disabled, padding, margin, border, shadow)  => {
console.log(title, color, disabled, padding, margin, border, shadow)
}
//调用
createButton('Sudongyu er', undefined /* optional color */, true ,'2px....', undefined /* optional margin*/);

使用对象的形式来传入数据也许会变得更容易观看

1
2
3
4
5
6
7
8
9
const createButton = ({title, color, disabled, padding, margin, border, shadow})  => {
console.log(title, color, disabled, padding, margin, border, shadow)
}

createButton({
title: 'Sudongyu er',
disabled: true,
shadow: '2px....'
});

不知道大家有没有在用异步请求的时候,写过下面的代码

1
2
3
4
5
6
7
8
9
axios.get('/api/xxx',params:{
mydata
},(res)=>{
let res = res.data;
if(res.code === 200){
//doSomething
res.data.records...
}
})

使用数据解构的方式让代码更加清晰

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
axios.get('/api/xxx',
params:{
mydata
},({
data:{
code,
msg,
foodData:{
records
}
}
})=>{
if(code === 200){
//doSomething...
}
}
)

对象数组解构

也许大家都知道这样的解构

1
2
3
4
5
6
7
const user = {
name: 'Sudongyu',
email: 'hi@xxx',
designation: 'Software Architect',
loves: 'The Code'
}
const {name, email, loves} = user;

另外一个例子,能显示很多问题

1
2
3
4
5
6
7
8
9
10
11
const getDetails = () => {
return ['xxx', 'sudongyu', 'Some Street', 'Some City', 'Some Zip', 'Some Country'];
}

const details = getDetails();
const uName = details[0];
const uEmail = details[1];
const uAddress = `${details[2]}, ${details[3]}, ${details[4]}, ${details[5]}`;
const uFirstName = uName.split(' ')[0];
const uLastName = uName.split(' ')[1];

非常的恶心,下面来看解构代码

1
2
3
4
5
6
7
8
9
10
11
12
13
const getDetails = () => {
return ['xxx', 'sudongyu', 'Some Street', 'Some City', 'Some Zip', 'Some Country'];
}
const [uName, uEmail, ...uAddressArr] = getDetails();
const uAddress = uAddressArr.join(', ');
const [uFirstName, uLastName] = uName.split('');
console.log({
uFirstName,
uLastName,
uEmail,
uAddress
});

我们可以通过上面的代码了解到对于一条数据的解构时机

  1. 需要直接获取就能使用的数据,直接解构
  2. 需要获取之后通过相同功能加工的数据,用剩余参数解构出来加工
  3. split之后的数据,建议直接用数组解构

总之就是减少数组下标的访问

避免硬编码

看例子

1
2
3
4
setInterval(() => {
// do something
}, 86400000);
// WHAT IS THIS 86400000 ??? 🤔

看代码的人可能不知道这个数字代表什么以及他是如何计算的,以及他背后的业务逻辑是什么,我们可以创建一个常量来代替它。

1
2
3
4
5
6
7
const DAY_IN_MILLISECONDS = 3600 * 24 * 1000; // 86400000

setInterval(() => {
// do something
}, DAY_IN_MILLISECONDS);
// now this makes sense

看另外一个例子

1
2
3
4
5
6
7
const createUser = (name, designation, type) => {
console.log({name, designation, type});
}

createUser('SudongYu', 'Software Architect', '1');
// WHAT IS this '1'? 🤔

这里这个1很难让人理解代表的含义是什么,即type这是什么用户。因此,我们可以创建一个我们拥有的用户类型的对象映射,而不是在这里对值进行硬编码’1’,如下所示:

1
2
3
4
5
6
7
8
9
10
11
const USER_TYPES = {
REGULAR_EMPLOYEE: '1'
}

const createUser = (name, designation, type) => {
console.log({name, designation, type});
}

createUser('Sudongyu', 'Software Architect', USER_TYPES.REGULAR_EMPLOYEE);
// smoooooooth 😎

避免使用简写变量名

速记变量在需要它们的地方才有意义。就像如果你有像xand这样的位置坐标y,那是可行的。p但是,如果我们在没有上下文的情况下创建像,t之类的变量c,那么真的很难阅读、跟踪和维护这样的代码。例如看这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
const t = 25;

let users = ['Sudongyuer', 'xxx'];

users = users.map((user) => {
...
return {
...user,
tax: user.salary * t / 100 // WHAT IS `t` again? 🤔
}
})

上面的例子表明,现在开发人员/读者必须一直向上滚动或转到定义来尝试理解这个变量是什么。因此是不干净的代码😠。这也称为对变量进行思维导图,其中只有作者知道它们的含义。因此,我们可以给它一个适当的名称,而不是简写变量名称,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
const taxFactor = 25;

let users = ['Sudongyuer', 'xxx'];

users = users.map((user) => {
// some code
return {
...user,
tax: user.salary * taxFactor / 100
}
})

设置默认对象值

在某些情况下,你可能希望你的对象能够提供一些默认值,如果没有就使用传入的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const createButton = ({title, color, disabled, padding})  => {
const button = {};
button.color = color || '#333';
button.disabled = disabled || false;
button.title = title || '';
button.padding = padding || 0;
return button;
}

const buttonConfig = {
title: 'Click me!',
disabled: true
}

const newButton = createButton(buttonConfig);
console.log('newButton', newButton)

修改后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const createButton = (config)  => {
return {
...{
color: '#dcdcdc',
disabled: false,
title: '',
padding: 0
},
...config
};
}

const buttonConfig = {
title: 'Click me!',
disabled: true
}

const newButton = createButton(buttonConfig);
console.log('newButton', newButton)

使用方法链

如果我们知道类/对象的用户将一起使用多个函数,则方法链接是一种很有用的技术。您可能已经在诸如 moment.js 之类的库中看到了这一点。让我们看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Player {
constructor (name, score, position) {
this.position = position;
this.score = score;
this.name = name;
}
setName(name) {
this.name = name;
}
setPosition(position) {
this.position = position;
}
setScore(score) {
this.score = score;
}
}

const player = new Player();
player.setScore(0);
player.setName('Sudongyuer');
player..setPosition([2, 0]);
console.log(player)

你可以看到我们需要为这个实例调用一系列的函数,这看起来有点不太美观,修改后的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Player {
constructor (name, score, position) {
this.position = position;
this.score = score;
this.name = name;
}
setName(name) {
this.name = name;
return this; // <-- THIS
}
setPosition(position) {
this.position = position;
return this; // <-- THIS
}
setScore(score) {
this.score = score;
return this; // <-- THIS
}
}

const player = new Player();
player.setScore(0).setName('Sudongyuer').setPosition([2, 0]);
// SUPER COOL 😎
console.log(player)

该方法的核心是每个方法都返回this对象,让他能够链式调用

在回调上使用Promise

回调地狱.jpg

看例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
 const getSocials = (callback) => {
setTimeout(() => {
callback({socials: {youtube: 'xxx', twitter: 'xxx'}});
}, 1500);
}

const getBooks = (callback) => {
setTimeout(() => {
callback({books: ['React Cookbook']});
}, 1500);
}

const getDesignation = (callback) => {
setTimeout(() => {
callback({designation: 'Software Architect'});
}, 1500);
}

const getUser = (callback) => {
setTimeout(() => {
callback({user: 'Sudongyuer'});
}, 1500);
}

getUser(({user}) => {
console.log('user retrieved', user)
getDesignation(({designation}) => {
console.log('designation retrieved', designation)
getBooks(({books}) => {
console.log('books retrieved', books)
getSocials(({socials}) => {
console.log('socials retrieved', socials)
})
})
})
})

这代码看起来非常不顺眼,如果有大量的异步任务,写起来会更加难看,特别是缩进。为了更好的可读性,我们使用Promise包装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
const getSocials = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve({socials: {youtube: 'xxx', twitter: 'xxx'}});
}, 1500);
})

}

const getBooks = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve({books: ['React Cookbook']});
}, 1500);
})
}

const getDesignation = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve({designation: 'Software Architect'});
}, 1500);
})
}

const getUser = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve({user: 'Sudongyuer'});
}, 1500);
})
}

getUser()
.then(({user}) => {
console.log('user retrieved', user);
return getDesignation();
})
.then(({designation}) => {
console.log('designation retrieved', designation)
return getBooks();
})
.then(({books}) => {
console.log('books retrieved', books);
return getSocials();
})
.then(({socials}) => {
console.log('socials retrieved', socials)
})

这样的话,很大程度就解决了缩进的问题,而且也比较美观,当然我们还有更具可读性的方法,利用到async和await

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
const getSocials = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve({socials: {youtube: 'xxx', twitter: 'xxx'}});
}, 1500);
})

}

const getBooks = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve({books: ['React Cookbook']});
}, 1500);
})
}

const getDesignation = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve({designation: 'Software Architect'});
}, 1500);
})
}

const getUser = () => {
return new Promise(resolve => {
setTimeout(() => {
resolve({user: 'Sudongyuer'});
}, 1500);
})
}

const performTasks = async () => {
const {user} = await getUser();
console.log('user retrieved', user);

const {designation} = await getDesignation();
console.log('designation retrieved', designation);

const {books} = await getBooks();
console.log('books retrieved', books);

const {socials} = await getSocials();
console.log('socials retrieved', socials);
}

```

# 明确函数意图
对于返回的是true/false的函数,最好以should/can/is/has开头
```javascript
// 👎 自我感觉良好的缩写:
let rContent = 'willen'

// 👍 无需对每个变量都写注释,从名字上就看懂
let firstName = 'jackie'

// 👎 从命名无法知道返回值类型
function showFriendsList({....} // // 无法辨别函数意图,返回的是一个数组,还是一个对象,还是true or false?

// 👍 明确函数意图,对于返回true or false的函数,最好以should/is/can/has开头
function shouldShowFriendsList({...}
function isEmpty({...}
function canCreateDocuments({...}
function hasLicense({...}
function sendEmailToUser(user{.... } //动词开头,函数意图就很明显

变量兜底

1
2
3
4
5
6
7
8
// 👎 对于求值获取的变量,没有兜底
const { data } = getApiRequest();
data.map((s) => s.id); //没有考虑data异常的情况,代码一跑就爆炸

// 👍 对于求值变量,做好兜底
const { data = [] } = getApiRequest();
data.map((s) => s?.id); //没有考虑data异常的情况,代码一跑就爆炸

待补充