JavaScript 规范整理

JavaScript 规范整理

场景

圣人走过的道路,荆棘遍布,火焰片片焚烧……

日常 review 代码时看到一些奇怪的代码,这里记录一下重构方案以及原因。

命名规范

不要使用拼音命名

如果不熟悉英语,可以使用 Codelf 或者 Google 翻译,避免使用拼音命名。

错误示例

1
2
// 这里是用户状态
const yongHuZhuangTai = 1

正确示例

1
const userStatus = 1

函数中的变量

js 中普通变量使用 小写开头驼峰命名法,而非不区分大小写,或使用下划线命名等等。

错误示例

1
2
// 用户操作日志备注
const useroperatinglogremark = '新增用户'

正确示例

1
const userOperatingLogRemark = '新增用户'

内部变量

如果需要不想让使用者使用的属性(能够看到),需要使用下划线开头。例如 _value,代表内部的值,外部不应该直接访问(实际上可以做到)。

1
2
3
4
5
6
7
8
9
10
11
12
class DateFormat {
constructor(fmt) {
// 不想让外部使用
this._fmt = fmt
}
format(date) {
// 具体格式化代码
}
parse(str) {
// 具体解析代码
}
}

不要使用无意义的前缀命名

如果一个对象的变量名已经很好的标识了该对象,那么内部的属性就不能使用对象名作为前缀!

错误示例

1
2
3
4
5
// 活跃的日志信息
const activeLog = {
activeUserId: 'rx',
activeTime: new Date(),
}

正确示例

1
2
3
4
const activeLog = {
userId: 'rx',
time: new Date(),
}

ES6

优先使用 const/let

一般情况下,使用 const/let 声明变量,而不是使用 var。因为使用 var 声明的变量会存在变量提升。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
;(function() {
// 使用 var 声明的变量(初始值为 undefined)
console.log(i)
i = 1
console.log(i)
// 此时使用 var 声明的变量 i 相当于在 function 顶部声明,然后在此处进行了赋值操作
var i = 0

// 使用 const 声明的变量(抛出异常 k is not defined)
// console.log(k)
k = 1
const k = 0
})()

关于可以参考 let 与 var 在 for 循环中的区别

使用新的函数声明方式

ES6 推出了一种更简洁的函数声明方式,不需要在写 function,只要 名字 + () 即可在 classObject 中声明函数。

错误示例

1
2
3
4
5
6
const user = {
name: 'rx',
hello: function() {
console.log('hello' + this.name)
},
}

正确示例

1
2
3
4
5
6
const user = {
name: 'rx',
hello() {
console.log('hello' + this.name)
},
}

优先使用箭头函数而非 function

优先使用 箭头函数 而不是使用传统的函数,尤其是使用 匿名函数 时,更应如此。

错误示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const sum = [1, 2, 3, 4]
// 过滤出偶数
.filter(function(i) {
return i % 2 === 0
})
// 将偶数翻倍
.map(function(i) {
return i * 2
})
// 计算总和
.reduce(function(res, i) {
return (res += i)
})

console.log(sum)

正确示例

1
2
3
4
5
6
7
8
9
const sum = [1, 2, 3, 4]
// 过滤出偶数
.filter(i => i % 2 === 0)
// 将偶数翻倍
.map(i => i * 2)
// 计算总和
.reduce((res, i) => (res += i))

console.log(sum)

不要使用 if 判断再赋予默认值

如果函数需要对参数做默认值处理,请不要使用 if 判空之后再修改参数,而是使用 ES6 的 默认参数解构赋值

主要优点

  • 减少代码,JavaScript 是动态语言,维护起来较为麻烦,代码越少,错误越少
  • 清晰明了,可以让人一眼就能看出这个参数的默认值,而不需要关心函数内部的逻辑
  • IDE 大多对此进行了支持,代码提示时便会告诉我们参数是可选的并且有默认值

错误示例

1
2
3
4
5
6
7
8
9
10
11
/**
* 格式化日期
* @param {Date} [date] 日期对象。默认为当前时间
* @return {String} 格式化日期字符串
*/
function formatDate(date) {
if (date === undefined) {
date = new Date()
}
return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
}

正确示例

1
2
3
function formatDate(date = new Date()) {
return `${date.getFullYear()}-${date.getMonth() + 1}-${date.getDate()}`
}

这里如果展开来讲实在太多,请参考 JavaScript 善用解构赋值

优先使用 Map 做键值对映射而非传统的对象

如果需要 键值映射,不要使用一般的对象,而是用 ES6 的 Map。它不仅可以使用 任意类型的键,另外 Map 本身也是 有序 的哦

错误示例

1
2
3
4
5
6
7
8
9
const obj = {
2: '琉璃',
1: 'rx',
'1': 'liuli',
}
// 结果为 true,因为属性 1 实际上会被转换为 '1'
console.log(obj[1] === obj['1'])
// 结果为 [ '1', '2'],因为是按照属性字符串排序的
console.log(Object.keys(obj))

正确示例

1
2
3
4
5
6
7
8
const map = new Map()
.set(2, '琉璃')
.set(1, 'rx')
.set('1', 'liuli')
// 结果为 false
console.log(map.get(1) === map.get('1'))
// 结果为 [ 2, 1, '1' ],因为是按照插入顺序排序的
console.log(Array.from(map.keys()))

优先使用模板字符串拼接多个字符串变量

如果需要拼接多个对象以及字符串时,不要使用 + 进行拼接,使用 es6 的 模板字符串 会更好一点。一般而言,如果需要拼接的变量超过 3 个,那么就应该使用模板字符串了。

错误示例

1
2
3
function hello(name, age, sex) {
return 'name: ' + name + ', age: ' + age + ', sex: ' + sex
}

正确示例

1
2
3
function hello(name, age, sex) {
return `name: ${name}, age: ${age}, sex: ${sex}`
}

当独立参数超过 3 个时使用对象参数并解构

错误示例

1
2
3
function hello(name, age, sex) {
return `name: ${name}, age: ${age}, sex: ${sex}`
}

正确示例

1
2
3
function hello({ name, age, sex }) {
return `name: ${name}, age: ${age}, sex: ${sex}`
}

不要写多余的 await

如果 await 是不必要的(在返回语句时,那么就不要用 async 标识函数),这是没有必要的 – 除非,你需要在这个函数内异步操作完成后有其他操作。

错误示例

1
2
3
4
5
6
7
8
9
10
11
12
const login = async ({ username, password }) => {
if (!useranme) {
console.log('用户名不能为空')
return
}
if (!password) {
console.log('密码不能为空')
return
}
// 真正发起登录请求
return await userApi.login(user)
}

正确示例

1
2
3
4
5
6
7
8
9
10
11
12
const login = ({ username, password }) => {
if (!useranme) {
console.log('用户名不能为空')
return
}
if (!password) {
console.log('密码不能为空')
return
}
// 真正发起登录请求
return userApi.login(user)
}

不要使用 == 进行比较

在 js 中使用 == 比较相当危险,你永远不知道 js 到底是按照什么类型比较的,因为 js 会做各种隐式转换。而如果使用 === 比较,则会同时比较 类型 是否都相同,避免了各种不确定的问题。

错误示例

1
2
3
4
5
console.log(1 == true) // true
console.log(1 == '1') // true
console.log('1' == true) // true
console.log('0' == true) // false
console.log([] == []) // false

扪心自问,你真的知道上面为什么会出现这种结果么?即便知道,对于其他人而言仍然是难以预测的,所以抛弃掉 == 吧,学会使用更好的 === 最好

1
2
3
4
5
console.log(1 == true) // false
console.log(1 == '1') // false
console.log('1' == true) // false
console.log('0' == true) // false
console.log([] == []) // false

使用计算属性名替代使用方括号表示法赋值

目前而言已经有了 计算属性名 用以在初始化时计算属性名,所以不需要再先声明对象再使用 方括号表示法 进行赋值了。

ES5 写法

1
2
3
4
5
6
7
const state = {
'user.username': function() {},
}

state[Date.now()] = new Date()

console.log(state)

ES6 写法

1
2
3
4
5
6
const state = {
'user.username'() {},
[Date.now()]: new Date(),
}

console.log(state)

简单的选项列表优先使用 Map 而非数组

对于复选框,想必很多人相当熟悉。下面使用 js 模拟一个复选框

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const item = {
id: 1,
role: ['1', '2'],
name: '',
}
const options = [
{
roleid: '1',
label: '黄金糕',
},
{
roleid: '2',
label: '双皮奶',
},
{
roleid: '3',
label: '蚵仔煎',
},
]

现在的需求是根据 role 计算显示值 name 的值

1
2
3
4
item.name = item.role
.map(role => options.find(op => op.roleid === role))
.filter(s => s)
.join(',')

但实际上这里应该使用 Map 替代数组,因为数组的 find 其实非常低效,也需要进行遍历,使用 Map 的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const item = {
id: 1,
role: [1, 2],
name: '',
}
const options = new Map()
.set(1, '黄金糕')
.set(2, '双皮奶')
.set(3, '蚵仔煎')

function calcName(role) {
return role
.map(k => options.get(k))
.filter(s => s)
.join(',')
}

item.name = calcName(item.role)

可以看到,获取时使用了 Map#get,在效率上应该是极好的。

附: 该问题来自 https://segmentfault.com/q/1010000019426996

存放 id 标识列表使用 Set 而非数组

还是上面的例子,当你需要存取当前选中复选框的值 role 使用数组时,有可能遇到 id 重复的问题,实际上导致每次添加前需要使用 Array#includes 判断是否已存在。这里可以使用 Set 从数据结构层面避免掉可能重复的问题。

修改后的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const item = {
id: 1,
role: new Set([1, 2]),
name: '',
}
const options = new Map()
.set(1, '黄金糕')
.set(2, '双皮奶')
.set(3, '蚵仔煎')

function calcName(role) {
return Array.from(role)
.map(k => options.get(k))
.filter(s => s)
.join(',')
}

item.name = calcName(item.role)

先判断是否存在执行某些操作也非常方便,可以使用 Set#has 进行判断,当然时间复杂度时 O1。

逻辑代码

不要判断一个 Boolean 值并以此返回 Boolean 值

不要在得到一个 Boolean 的值后使用 if-else 进行判断,然后根据结果返回 truefalse,这真的显得非常非常蠢!

错误示例

1
2
3
4
5
6
7
8
9
// 模拟登录异步请求
const login = async ({ username, password }) => {
const res = username === 'rx' && password === 'rx'
if (res) {
return true
} else {
return false
}
}

正确示例

1
2
3
// 模拟登录异步请求
const login = async ({ username, password }) =>
username === 'rx' && password === 'rx'

不要使用多余的变量

如果一个表达式立刻被使用并且只会被使用一次,那就不要使用变量声明,直接在需要的地方使用好了。

错误示例

1
2
3
4
5
// 模拟登录异步请求
const login = async ({ username, password }) => {
const res = username === 'rx' && password === 'rx'
return res
}

正确示例

1
2
3
4
// 模拟登录异步请求
const login = async ({ username, password }) => {
return username === 'rx' && password === 'rx'
}

不要使用嵌套 if

不要使用多级的 if 嵌套,这会让代码变得丑陋且难以调试,应当优先使用 提前 return 的策略。

错误示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 模拟登录异步请求
const login = async ({ username, password }) =>
username === 'rx' && password === 'rx'

async function submit(user) {
const { username, password } = user
if (username) {
if (password) {
const res = await login(user)
if (res) {
console.log('登录成功,即将跳转到首页')
} else {
console.log('登录失败,请检查用户名和密码')
}
} else {
console.log('用户密码不能为空')
}
} else {
console.log('用户名不能为空')
}
}

正确示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 模拟登录异步请求
const login = async ({ username, password }) =>
username === 'rx' && password === 'rx'

async function submit(user) {
const { username, password } = user
if (!username) {
console.log('用户名不能为空')
return
}
if (!password) {
console.log('用户密码不能为空')
return
}
const res = await login(user)
if (!res) {
console.log('登录失败,请检查用户名和密码')
return
}
console.log('登录成功,即将跳转到首页')
}

不要先声明空对象然后一个个追加属性

有时候会碰到这种情况,先声明一个空对象,然后在下面一个个追加属性,为什么创建对象与初始化不放到一起做呢?

错误示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 模拟登录异步请求
const login = async ({ username, password }) =>
username === 'rx' && password === 'rx'

async function submit(username, password) {
// 数据格式校验处理。。。

const user = {}
user.username = username.trim()
user.password = password.trim()

const res = await login(user)

// 后续的错误处理。。。
}

正确示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 模拟登录异步请求
const login = async ({ username, password }) =>
username === 'rx' && password === 'rx'

async function submit(username, password) {
// 数据格式校验处理。。。

const user = {
username: username.trim(),
password: password.trim(),
}

const res = await login(user)

// 后续的错误处理。。。
}

不要使用无意义的函数包裹

使用函数时,如果你想包裹的函数和原来函数的参数/返回值相同,那就直接应该使用函数作为参数,而非在包裹一层。给人的感觉就像是大夏天穿着棉袄吃雪糕 – 多此一举!

错误示例

1
2
3
4
// 判断是否是偶数的函数
const isEven = i => i % 2 === 0
//过滤出所有偶数
const res = [1, 2, 3, 4].filter(i => isEven(i))

正确示例

1
2
3
4
// 判断是否是偶数的函数
const isEven = i => i % 2 === 0
//过滤出所有偶数
const res = [1, 2, 3, 4].filter(isEven)

不要使用三元运算符进行复杂的计算

三元运算符适合于替代简单的 if-else 的情况,如果碰到较为复杂的情况,请使用 if + return 或者解构/默认参数的方式解决。

错误示例

1
2
3
4
5
6
7
8
function formatUser(user) {
return user === undefined
? 'username: noname, password: blank'
: 'username: ' +
(user.username === undefined ? 'noname' : user.username) +
', password: ' +
(user.password === undefined ? 'blank' : user.password)
}

看到上面的代码就感觉到一股烂代码的味道扑面而来,这实在是太糟糕了!实际上只需要两行代码就好了!

正确示例

1
2
3
function formatUser({ username = 'noname', password = 'blank' } = {}) {
return `username: ${username}, password: ${password}`
}

如果变量有所关联则使用对象而非多个单独的变量

如果变量有所关联,例如一个表单,存储的时候不要使用单独的变量,将之存储到一个表单变量中更好。

错误示例

1
2
3
4
5
6
7
8
9
10
11
12
13
function login(){
const username = document.querySelector('#username')
const password = document.querySelector('#password')
const remeberMe = document.querySelector('#remeberMe')

// 一些校验。。。
if (!validate(username, password, remeberMe)) {
return
}
// 请求后台
const res = await userLogin.login(username, password, remeberMe)
// 后处理。。。
}

正确示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function login(){
const user = {
username : document.querySelector('#username'),
password : document.querySelector('#password'),
remeberMe : document.querySelector('#remeberMe'),
}
// 一些校验。。。
if (!validate(user)) {
return
}
// 请求后台
const res = await userLogin.login(user)
// 后处理。。。
}

应该尽量解决编辑器警告

如果编辑器对我们的代码发出警告,那么一般都是我们代码出现了问题(一般开发人员的能力并不足以比肩编辑器 #MS 那些 dalao 的能力)。所以,如果出现了警告,应该先去解决它 – 如果你确认发生了错误,则通过注释/配置禁用它!

使用类型定义参数对象

如果一个函数需要一个对象参数,最好专门定义一个类型,并在注释上说明,便于在使用时 IDE 进行提示,而不需要去查找文档手册。

错误示例

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 格式化用户
* @param {Object} user 格式化的用户对象
*/
function formatUser(user) {
const { username, password } = user || {}
return `user, username: ${username}, password: ${password}`
}

// 此处别人并不知道 User 里面到底有什么属性,只能去查看文档
const str = formatUser({ username: 'rx', password: '123456' })
console.log(str)

正确示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class User {
constructor(username, password) {
this.username = username
this.password = password
}
}

/**
* 格式化用户
* @param {User} user 格式化的用户对象
*/
function formatUser(user) {
const { username, password } = user || {}
return `user, username: ${username}, password: ${password}`
}

const str = formatUser(new User('rx', '123456'))
console.log(str)

尽量扁平化代码

尽量将 a 调用 b, b 调用 c,然后 b 调用 d,优化为依次调用 a, b, c, d

注意: 这里使用的是尽量而非不要,深层嵌套不可避免,但在局部上,应该采取扁平化的策略,提前 return 避免嵌套 if-else 是个很好的例子。

自执行函数前面必须加分号

如果我们需要使用自执行函数,则开头必须加上 ; 以避免可能出现的歧义。

错误示例

1
2
3
4
5
6
7
function returnItself(o) {
return o
}
returnItself(() => console.log(1))
(() => {
console.log(2)
})()

上面这段代码是有问题的,因为后面的自执行函数会被认为是上一句的 returnItself 返回函数的参数,最后的括号会被认为又是一次调用,将会抛出错误

1
returnItself(...)(...) is not a function

正确示例

1
2
3
4
5
6
7
function returnItself(o) {
return o
}
returnItself(() => console.log(1))
;(() => {
console.log(2)
})()

使用分号可以明确告诉 JavaScript 这是一行新的代码,上面的代码已经到此为止了。