变量、常量
- 驼峰(使用有意义且可发音)命名变量、函数名、方法名
- 大写命名常量(单词间用“_”)
- 使用解释变量(占位符)【_】
- 使用默认参数代替短路或条件
函数、方法
- 使用对象做函数参数(理性情况参数个数为2个或更少)
- forEach前先过滤,不在forEach中做判断(函数应该只做一件事)
- 函数应该只有一个抽象
- 删除重复代码
- 使用Object.assign设置默认对象
- 不要将标示(用作判断的东西)用作函数参数
- 不写入全局函数
- 支持函数式编程而不是命令式编程
- 封装条件语句
- 避免否定条件句
- 避免条件句(使用多态性来实现相同的任务)
- 避免类型检查(typeof、instanceof)(可借助typescript)
- 不要过度优化
- 删除死代码
对象和数据结构
- 使用getters和setters
- 使对象具有私有成员(可以通过闭包实现、private)
类(Class)
- 与ES5普通功能相比,推荐使用ES2015/ES6级别
- 使用方法链接(return this)
- 选择组合而不是继承
- 单一责任原则(SRP)
正如Clean代码中所述,“一个类的更改原因不应该超过一个”。将一个类塞满大量功能是很诱人,例如当你在你的航班上只能带一个手提箱。这样做的问题是,你的类在概念上没有凝聚力,它会给它很多改变的理由。尽可能减少更改类的次数非常重要。这一点很重要,因为如果一个类中有太多的功能,而您修改了其中的一部分,那么很难理解这将如何影响代码库中的其他依赖模块。
Bad:
class UserSettings {
constructor(user) {
this.user = user;
}
changeSettings(settings) {
if (this.verifyCredentials()) {
// ...
}
}
verifyCredentials() {
// ...
}
}
Good:
class UserAuth {
constructor(user) {
this.user = user;
}
verifyCredentials() {
// ...
}
}
class UserSettings {
constructor(user) {
this.user = user;
this.auth = new UserAuth(user);
}
changeSettings(settings) {
if (this.auth.verifyCredentials()) {
// ...
}
}
}
- 开/关原理(OCP)
正如bertrandmeyer所说,“软件实体(类、模块、函数等)应该是开放的,但是对于修改是关闭的。”这意味着什么呢?这个原则基本上是说,您应该允许用户在不改变现有代码的情况下添加新功能。
Bad:
class AjaxAdapter extends Adapter {
constructor() {
super();
this.name = "ajaxAdapter";
}
}
class NodeAdapter extends Adapter {
constructor() {
super();
this.name = "nodeAdapter";
}
}
class HttpRequester {
constructor(adapter) {
this.adapter = adapter;
}
fetch(url) {
if (this.adapter.name === "ajaxAdapter") {
return makeAjaxCall(url).then(response => {
// transform response and return
});
} else if (this.adapter.name === "nodeAdapter") {
return makeHttpCall(url).then(response => {
// transform response and return
});
}
}
}
function makeAjaxCall(url) {
// request and return promise
}
function makeHttpCall(url) {
// request and return promise
}
Good:
class AjaxAdapter extends Adapter {
constructor() {
super();
this.name = "ajaxAdapter";
}
request(url) {
// request and return promise
}
}
class NodeAdapter extends Adapter {
constructor() {
super();
this.name = "nodeAdapter";
}
request(url) {
// request and return promise
}
}
class HttpRequester {
constructor(adapter) {
this.adapter = adapter;
}
fetch(url) {
return this.adapter.request(url).then(response => {
// transform response and return
});
}
}
- 里斯科夫Liskov替代原理(LSP)
这是一个非常简单的概念的可怕术语。它的正式定义是“如果s是T的一个子类型,那么T类型的对象可以被s类型的对象替换(即,s类型的对象可以替换T类型的对象),而不会改变该程序的任何期望属性(正确性、执行的任务等)”,这是一个更可怕的定义。
最好的解释是,如果有父类和子类,那么基类和子类可以互换使用,而不会得到错误的结果。这可能仍然令人困惑,所以让我们看看经典的方形矩形示例。从数学上讲,正方形是一个矩形,但如果通过继承使用“is-a”关系对其进行建模,则很快就会遇到麻烦。
Bad:
class Rectangle {
constructor() {
this.width = 0;
this.height = 0;
}
setColor(color) {
// ...
}
render(area) {
// ...
}
setWidth(width) {
this.width = width;
}
setHeight(height) {
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Rectangle {
setWidth(width) {
this.width = width;
this.height = width;
}
setHeight(height) {
this.width = height;
this.height = height;
}
}
function renderLargeRectangles(rectangles) {
rectangles.forEach(rectangle => {
rectangle.setWidth(4);
rectangle.setHeight(5);
const area = rectangle.getArea(); // BAD: Returns 25 for Square. Should be 20.
rectangle.render(area);
});
}
const rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles(rectangles);
Good:
class Shape {
setColor(color) {
// ...
}
render(area) {
// ...
}
}
class Rectangle extends Shape {
constructor(width, height) {
super();
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
class Square extends Shape {
constructor(length) {
super();
this.length = length;
}
getArea() {
return this.length * this.length;
}
}
function renderLargeShapes(shapes) {
shapes.forEach(shape => {
const area = shape.getArea();
shape.render(area);
});
}
const shapes = [new Rectangle(4, 5), new Rectangle(4, 5), new Square(5)];
renderLargeShapes(shapes);
- 接口隔离原理
JavaScript没有接口,所以这个原则并不像其他原则那样严格适用。然而,即使在JavaScript缺少类型系统的情况下,它也非常重要和相关。
ISP声明“不应该强迫客户端依赖于他们不使用的接口。”接口是JavaScript中的隐式契约,因为duck类型。
在JavaScript中演示这一原理的一个很好的例子是针对需要大型设置对象的类。不要求客户端设置大量的选项是有益的,因为大多数时候他们不需要所有的设置。将它们设为可选有助于防止出现“胖接口”。
Bad:
class DOMTraverser {
constructor(settings) {
this.settings = settings;
this.setup();
}
setup() {
this.rootNode = this.settings.rootNode;
this.settings.animationModule.setup();
}
traverse() {
// ...
}
}
const $ = new DOMTraverser({
rootNode: document.getElementsByTagName("body"),
animationModule() {} // Most of the time, we won't need to animate when traversing.
// ...
});
Good:
class DOMTraverser {
constructor(settings) {
this.settings = settings;
this.options = settings.options;
this.setup();
}
setup() {
this.rootNode = this.settings.rootNode;
this.setupOptions();
}
setupOptions() {
if (this.options.animationModule) {
// ...
}
}
traverse() {
// ...
}
}
const $ = new DOMTraverser({
rootNode: document.getElementsByTagName("body"),
options: {
animationModule() {}
}
});
- 依赖倒置原理(DIP)
这一原则阐述了两个基本事项:
1、高级模块不应依赖于低级模块。两者都应该依赖于抽象。
2、抽象不应依赖细节。细节应该依靠抽象。
这一点一开始可能很难理解,但如果您使用过AngularJS,您就会看到依赖注入(DI)形式的这一原则的实现。虽然它们不是完全相同的概念,但是DIP阻止高级模块了解其低级模块的细节并设置它们。它可以通过DI实现这一点。这样做的一个巨大好处是减少了模块之间的耦合。耦合是一种非常糟糕的开发模式,因为它使代码很难重构。
如前所述,JavaScript没有接口,因此依赖的抽象是隐式契约。也就是说,一个对象/类向另一个对象/类公开的方法和属性。在下面的示例中,隐式约定是InventoryTracker的任何请求模块都将具有一个requestItems方法。
Bad:
class InventoryRequester {
constructor() {
this.REQ_METHODS = ["HTTP"];
}
requestItem(item) {
// ...
}
}
class InventoryTracker {
constructor(items) {
this.items = items;
// BAD: We have created a dependency on a specific request implementation.
// We should just have requestItems depend on a request method: `request`
this.requester = new InventoryRequester();
}
requestItems() {
this.items.forEach(item => {
this.requester.requestItem(item);
});
}
}
const inventoryTracker = new InventoryTracker(["apples", "bananas"]);
inventoryTracker.requestItems();
Good:
class InventoryTracker {
constructor(items, requester) {
this.items = items;
this.requester = requester;
}
requestItems() {
this.items.forEach(item => {
this.requester.requestItem(item);
});
}
}
class InventoryRequesterV1 {
constructor() {
this.REQ_METHODS = ["HTTP"];
}
requestItem(item) {
// ...
}
}
class InventoryRequesterV2 {
constructor() {
this.REQ_METHODS = ["WS"];
}
requestItem(item) {
// ...
}
}
// By constructing our dependencies externally and injecting them, we can easily
// substitute our request module for a fancy new one that uses WebSockets.
const inventoryTracker = new InventoryTracker(
["apples", "bananas"],
new InventoryRequesterV2()
);
inventoryTracker.requestItems();
测试
- 每个测试的单一概念
并发
- 使用Promises,而不是callbacks(回调)
- 使用Async/Await比使用Promises代码更干净
错误处理
- 不要忽略捕捉到的错误
- 不要忽视被拒绝(失败)的Promises(请求)
格式化
- 使用一致的大写字母
- 函数调用方和被调用方应接近(保持函数调用方在被调用方的上面)
注释
- 只注释具有业务逻辑复杂性的内容
- 不要在代码库中留下注释掉的代码
- 不需要版本更新、日志注释
- 避免使用位置标记