团队建设
JS 风格编写规范
00 分钟
2021-5-17
2023-9-11
type
status
date
slug
summary
tags
category
icon
password

变量

命名方式:小驼峰
命名规范:前缀名词

常量

命名方式:全部大写
命名规范:多个单词时使用分隔符_

函数

命名方式:小驼峰
命名规范:前缀动词
常用动词:can、has、is、load、get、set

命名方式:大驼峰
命名规范:前缀名词

注释

单行
多行

减少嵌套

确定条件不允许时,尽早返回。经典使用场景:校验数据

减少特定标记值

使用常量进行自解释

表达式

尽可能简洁表达式

分支较多处理

对于相同变量或表达式的多值条件,用switch代替if

使用变量名自解释 V1.1

逻辑复杂时,建议使用变量名自解释,而不是晦涩难懂的简写。

使用函数名自解释 V1.1

遵循单一职责的基础上,可以把逻辑隐藏在函数中,同时使用准确的函数名自解释。

其他规范

使用prettier格式化工具以及eslint校验
  • 格式自动化
  • 4个缩进
  • 全部单引号
  • 方法if / else / for / while / function / switch / do / try / catch / finally 关键字后有一个空格
  • 自动省略分号
.prettierrc配置:
.eslintrc.js规则:
如果是vue-cli3项目,以上配置的eslint插件默认已安装;如果不是vue-cli3项目,需要npm安装对应包:npm install --save-dev babel-eslint eslint-plugin-vue
 
 

变量

#使用有意义,可读性好的变量名

反例:
正例:

#使用 ES6 的 const 定义常量

反例中使用"var"定义的"常量"是可变的。
在声明一个常量时,该常量在整个程序中都应该是不可变的。
反例:
正例:

#对功能类似的变量名采用统一的命名风格

反例:
正例:

#使用易于检索名称

我们需要阅读的代码远比自己写的要多,使代码拥有良好的可读性且易于检索非常重要。阅读变量名晦涩难懂的代码对读者来说是一种相当糟糕的体验。 让你的变量名易于检索。
反例:
正例:

#使用说明变量(即有意义的变量名)

反例:
正例:

#不要绕太多的弯子

显式优于隐式。
反例:
正例:

#避免重复的描述

当类/对象名已经有意义时,对其变量进行命名不需要再次重复。
反例:
正例:
 

#避免无意义的条件判断

反例:
正例:

#函数

#函数参数 (理想情况下应不超过 2 个)

限制函数参数数量很有必要,这么做使得在测试函数时更加轻松。过多的参数将导致难以采用有效的测试用例对函数的各个参数进行测试。
应避免三个以上参数的函数。通常情况下,参数超过两个意味着函数功能过于复杂,这时需要重新优化你的函数。当确实需要多个参数时,大多情况下可以考虑这些参数封装成一个对象。
JS 定义对象非常方便,当需要多个参数时,可以使用一个对象进行替代。
反例:
正例:

#函数功能的单一性

这是软件功能中最重要的原则之一。
功能不单一的函数将导致难以重构、测试和理解。功能单一的函数易于重构,并使代码更加干净。
反例:
正例:
 

#函数名应明确表明其功能

反例:
正例:

#函数应该只做一层抽象

当函数的需要的抽象多于一层时通常意味着函数功能过于复杂,需将其进行分解以提高其可重用性和可测试性。
反例:
正例:

#移除重复的代码

永远、永远、永远不要在任何循环下有重复的代码。
这种做法毫无意义且潜在危险极大。重复的代码意味着逻辑变化时需要对不止一处进行修改。JS 弱类型的特点使得函数拥有更强的普适性。好好利用这一优点吧。
反例:
正例:

#采用默认参数精简代码

反例:
正例:

#使用 Object.assign 设置默认对象

反例:
正例:

#不要使用标记(Flag)作为函数参数

这通常意味着函数的功能的单一性已经被破坏。此时应考虑对函数进行再次划分。
反例:
正例:

#避免副作用

当函数产生了除了“接受一个值并返回一个结果”之外的行为时,称该函数产生了副作用。比如写文件、修改全局变量或将你的钱全转给了一个陌生人等。
程序在某些情况下确实需要副作用这一行为,如先前例子中的写文件。这时应该将这些功能集中在一起,不要用多个函数/类修改某个文件。用且只用一个 service 完成这一需求。
反例:
正例:

#不要写全局函数

在 JS 中污染全局是一个非常不好的实践,这么做可能和其他库起冲突,且调用你的 API 的用户在实际环境中得到一个 exception 前对这一情况是一无所知的。
想象以下例子:如果你想扩展 JS 中的 Array,为其添加一个 diff 函数显示两个数组间的差异,此时应如何去做?你可以将 diff 写入 Array.prototype,但这么做会和其他有类似需求的库造成冲突。如果另一个库对 diff 的需求为比较一个数组中首尾元素间的差异呢?
使用 ES6 中的 class 对全局的 Array 做简单的扩展显然是一个更棒的选择。
反例:
正例:

#采用函数式编程

函数式的编程具有更干净且便于测试的特点。尽可能的使用这种风格吧。
反例:
正例:

#封装判断条件

反例:
正例:

#避免“否定情况”的判断

反例:
正例:

#避免条件判断

这看起来似乎不太可能。
大多人听到这的第一反应是:“怎么可能不用 if 完成其他功能呢?”许多情况下通过使用多态(polymorphism)可以达到同样的目的。
第二个问题在于采用这种方式的原因是什么。答案是我们之前提到过的:保持函数功能的单一性。
反例:
正例:

#避免类型判断(part 1)

JS 是弱类型语言,这意味着函数可接受任意类型的参数。
有时这会对你带来麻烦,你会对参数做一些类型判断。有许多方法可以避免这些情况。
反例:
正例:

#避免类型判断(part 2)

如果需处理的数据为字符串,整型,数组等类型,无法使用多态并仍有必要对其进行类型检测时,可以考虑使用 TypeScript。
反例:
正例:

#避免过度优化

现代的浏览器在运行时会对代码自动进行优化。有时人为对代码进行优化可能是在浪费时间。
反例:
正例:

#删除无效的代码

不再被调用的代码应及时删除。
反例:
正例:

#对象和数据结构

#使用 getters 和 setters

JS 没有接口或类型,因此实现这一模式是很困难的,因为我们并没有类似 public 和 private 的关键词。
然而,使用 getters 和 setters 获取对象的数据远比直接使用点操作符具有优势。为什么呢?
  1. 当需要对获取的对象属性执行额外操作时。
  1. 执行 set 时可以增加规则对要变量的合法性进行判断。
  1. 封装了内部逻辑。
  1. 在存取时可以方便的增加日志和错误处理。
  1. 继承该类时可以重载默认行为。
  1. 从服务器获取数据时可以进行懒加载。
反例:
正例:

#让对象拥有私有成员

可以通过闭包完成
反例:
正例:

#

#单一职责原则 (SRP)

如《代码整洁之道》一书中所述,“修改一个类的理由不应该超过一个”。
将多个功能塞进一个类的想法很诱人,但这将导致你的类无法达到概念上的内聚,并经常不得不进行修改。
最小化对一个类需要修改的次数是非常有必要的。如果一个类具有太多太杂的功能,当你对其中一小部分进行修改时,将很难想象到这一修够对代码库中依赖该类的其他模块会带来什么样的影响。
反例:
正例:

#开/闭原则 (OCP)

“代码实体(类,模块,函数等)应该易于扩展,难于修改。”
这一原则指的是我们应允许用户方便的扩展我们代码模块的功能,而不需要打开 js 文件源码手动对其进行修改。
反例:
正例:

#利斯科夫替代原则 (LSP)

“子类对象应该能够替换其超类对象被使用”。
也就是说,如果有一个父类和一个子类,当采用子类替换父类时不应该产生错误的结果。
反例:
正例:

#接口隔离原则 (ISP)

“客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。”
在 JS 中,当一个类需要许多参数设置才能生成一个对象时,或许大多时候不需要设置这么多的参数。此时减少对配置参数数量的需求是有益的。
反例:
正例:

#依赖反转原则 (DIP)

该原则有两个核心点:
  1. 高层模块不应该依赖于低层模块。他们都应该依赖于抽象接口。
  1. 抽象接口应该脱离具体实现,具体实现应该依赖于抽象接口。
反例:
正例:

#使用 ES6 的 classes 而不是 ES5 的 Function

典型的 ES5 的类(function)在继承、构造和方法定义方面可读性较差。
当需要继承时,优先选用 classes。
但是,当在需要更大更复杂的对象时,最好优先选择更小的 function 而非 classes。
反例:
正例:

#使用方法链

这里我们的理解与《代码整洁之道》的建议有些不同。
有争论说方法链不够干净且违反了德米特法则 (opens new window),也许这是对的,但这种方法在 JS 及许多库(如 JQuery)中显得非常实用。
因此,我认为在 JS 中使用方法链是非常合适的。在 class 的函数中返回 this,能够方便的将类需要执行的多个方法链接起来。
反例:
 
正例:

#优先使用组合模式而非继承

在著名的设计模式 (opens new window)一书中提到,应多使用组合模式而非继承。
这么做有许多优点,在想要使用继承前,多想想能否通过组合模式满足需求吧。
那么,在什么时候继承具有更大的优势呢?这取决于你的具体需求,但大多情况下,可以遵守以下三点:
  1. 继承关系表现为"是一个"而非"有一个"(如动物->人 和 用户->用户细节)
  1. 可以复用基类的代码("Human"可以看成是"All animal"的一种)
  1. 希望当基类改变时所有派生类都受到影响(如修改"all animals"移动时的卡路里消耗量)
反例:
正例:

#测试

#单一的测试每个概念

反例:
正例:

#并发

#用 Promises 替代回调

回调不够整洁并会造成大量的嵌套。ES6 内嵌了 Promises,使用它吧。
反例:
正例:

#Async/Await 是较 Promises 更好的选择

Promises 是较回调而言更好的一种选择,但 ES7 中的 async 和 await 更胜过 Promises。
在能使用 ES7 特性的情况下可以尽量使用他们替代 Promises。
反例:
正例:

#错误处理

错误抛出是个好东西!这使得你能够成功定位运行状态中的程序产生错误的位置。

#别忘了捕获错误

对捕获的错误不做任何处理是没有意义的。
代码中 try/catch 的意味着你认为这里可能出现一些错误,你应该对这些可能的错误存在相应的处理方案。
反例:
正例:

#不要忽略被拒绝的 promises

理由同 try/catch
反例:
正例:

#格式化

格式化是一件主观的事。如同这里的许多规则一样,这里并没有一定/立刻需要遵守的规则。可以在这里 (opens new window)完成格式的自动化。

#大小写一致

JS 是弱类型语言,合理的采用大小写可以告诉你关于变量/函数等的许多消息。
这些规则是主观定义的,团队可以根据喜欢进行选择。重点在于无论选择何种风格,都需要注意保持一致性。
反例:
正例:

#调用函数的函数和被调函数应放在较近的位置

当函数间存在相互调用的情况时,应将两者置于较近的位置。
理想情况下,应将调用其他函数的函数写在被调用函数的上方。
反例:
正例:

#注释

#只对存在一定业务逻辑复杂性的代码进行注释

注释并不是必须的,好的代码是能够让人一目了然,不用过多无谓的注释。
反例:
正例:

#不要在代码库中遗留被注释掉的代码

版本控制的存在是有原因的。让旧代码存在于你的 history 里吧。
反例:
正例:

#不需要版本更新类型注释

记住,我们可以使用版本控制。废代码、被注释的代码及用注释记录代码中的版本更新说明都是没有必要的。
需要时可以使用 git log 获取历史版本。
反例:
正例:

#避免位置标记

这些东西通常只能代码麻烦,采用适当的缩进就可以了。
反例:
正例:

#避免在源文件中写入法律评论

将你的 LICENSE 文件置于源码目录树的根目录。
反例:
正例:

#补充

  • 大概率坏代码判断
    • 重复代码
    • 过长函数
    • 过大的类
    • 过长参数列表
  • 好的命名
  • 函数单一职责
  • 通过引入解释性变量或函数,使得表达更清晰
  • 更少的嵌套,尽早 return
  • 以多态取代条件表达式
  • 倾向于遍历对象而不是 Switch 语句
  • 使用默认参数和解构
  • 善于利用ES6 Array处理数组
    • Array.find()
    • Array.forEach(),Array.filter(),Array.map(),Array.reduce()
    • 数组rest运算符
    • 多重判断时使用 Array.includes
    • 对 所有/部分 判断使用 Array.every & Array.some
  • 善于利用ES6 Object处理对象
    • 对象解构
    • rest(ES9开始支持)
    • Object.assign()
    • Object.keys(),Object.values()

#参考文章

#参考链接