介绍
作者根据 Robert C. Martin 《代码整洁之道》总结了适用于 JavaScript 的软件工程原则。 《Clean Code JavaScript》
不必严格遵守本文的所有原则,仅仅只是一份建议。
一、变量(Variables)
1. 使用语义化变量名 ✅
小驼峰式命名方法命名规范 : 类型+对象描述的方式
const yyyymmdstr = moment().format("YYYY/MM/DD");
const currentDate = moment().format("YYYY/MM/DD");
2. const 定义常量 ✅
使用 const 声明一个常量,该常量在整个程序中都应该是不可变的。
var FIRST_US_PRESIDENT = "George Washington";
const FIRST_US_PRESIDENT = "George Washington";
3. 使用易于检索名称 ✅
消除魔法字符串(与代码形成强耦合的某一个具体的字符串或数值),由含义清晰的变量代替。
// 525600 是什么?
for (var i = 0; i < 525600; i++) {
runCronJob();
}
// Declare them as capitalized `var` globals.
const MINUTES_IN_A_YEAR = 525600;
for (var i = 0; i < MINUTES_IN_A_YEAR; i++) {
runCronJob();
}
4. 避免重复的描述 ✅
当类/对象名已经有意义时,对其属性命名不需要再次重复。
var Car = {
carMake: 'Honda',
carModel: 'Accord',
carColor: 'Blue'
};
function paintCar(car) {
car.carColor = 'Red';
}
var Car = {
make: 'Honda',
model: 'Accord',
color: 'Blue'
};
function paintCar(car) {
car.color = 'Red';
}
5. 解释型变量 ✅
当类/对象名已经有意义时,对其属性命名不需要再次重复。
const cityStateRegex = /^(.+)[,\\s]+(.+?)\s*(\d{5})?$/;
// 不知道两个参数代表什么含义
saveCityState(
cityStateRegex.match(cityStateRegex)[1],
cityStateRegex.match(cityStateRegex)[2]
);
const address = "One Infinite Loop, Cupertino 95014";
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/;
const [_, city, zipCode] = address.match(cityZipCodeRegex) || [];
// 清晰知道两个参数的含义
saveCityZipCode(city, zipCode);
二、函数(Functions)
1. 默认参数,代替短路求值 ✅
如果使用默认参数,函数将只为未定义的参数提供默认值。其他 “falsy” 值(如''、""、false、null、0和NaN)将不会替换为默认值。
function createMicrobrewery(name) {
const breweryName = name || "Hipster Brew Co.";
// ...
}
function createMicrobrewery(name = "Hipster Brew Co.") {
// ...
}
2. 参数少于 3 位 ✅
限制函数参数数量很有必要,这么做使得在测试函数时更加轻松。当确实需要多个参数时,考虑这些参数封装成一个对象。
function createMenu(title, body, buttonText, cancellable) {
// ...
}
createMenu("Foo", "Bar", "Baz", true);
var menuConfig = {
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
}
function createMenu(menuConfig) {
...
}
3. 单一职责 ✅
功能单一的函数易于重构、测试和理解
function emailClients(clients) {
clients.forEach(client => {
let clientRecord = database.lookup(client);
if (clientRecord.isActive()) {
email(client);
}
});
}
function emailClients(clients) {
clients.forEach(client => {
emailClientIfNeeded(client);
});
}
function emailClientIfNeeded(client) {
if (isClientActive(client)) {
email(client);
}
}
function isClientActive(client) {
let clientRecord = database.lookup(client);
return clientRecord.isActive();
}
4. 明确表明函数功能 ✅
function addToDate(date, month) {
// ...
}
const date = new Date();
// It's hard to tell from the function name what is added
addToDate(date, 1);
function addMonthToDate(month, date) {
// ...
}
const date = new Date();
addMonthToDate(1, date);
5. Object.assign 覆盖对象默认值 ✅
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);
const menuConfig = {
title: "Order",
// User did not include 'body' key
buttonText: "Send",
cancellable: true
};
function createMenu(config) {
let finalConfig = Object.assign(
{
title: "Foo",
body: "Bar",
buttonText: "Baz",
cancellable: true
},
config
);
return finalConfig
}
createMenu(menuConfig); // {title: "Order", body: "Bar", buttonText: "Send", cancellable: true}
6. 避免重复代码 ❌
重复的代码意味着逻辑变化时需要对不止一处进行修改。
function showDeveloperList(developers) {
developers.forEach(developer => {
const expectedSalary = developer.calculateExpectedSalary();
const experience = developer.getExperience();
const githubLink = developer.getGithubLink();
const data = {
expectedSalary,
experience,
githubLink
};
render(data);
});
}
function showManagerList(managers) {
managers.forEach(manager => {
const expectedSalary = manager.calculateExpectedSalary();
const experience = manager.getExperience();
const portfolio = manager.getMBAProjects();
const data = {
expectedSalary,
experience,
portfolio
};
render(data);
});
}
function showEmployeeList(employees) {
employees.forEach(employee => {
const expectedSalary = employee.calculateExpectedSalary();
const experience = employee.getExperience();
const data = {
expectedSalary,
experience
};
switch (employee.type) {
case "manager":
data.portfolio = employee.getMBAProjects();
break;
case "developer":
data.githubLink = employee.getGithubLink();
break;
}
render(data);
});
}
7. 避免 flag 作为参数 ❌
flag 值的使用,意味着这个函数做了不止一件事, 违反单一原则。若 flag 为 boolean值,应考虑对函数进行再次划分。
function createFile(name, temp) {
if (temp) {
fs.create(`./temp/${name}`);
} else {
fs.create(name);
}
}
function createFile(name) {
fs.create(name);
}
function createTempFile(name) {
createFile(`./temp/${name}`);
}
8. 避免函数副作用 ❌
副作用是指函数除了除了“接受一个值并返回一个结果”外,还进行了其他操作(如修改外部全局变量),这些操作就会产生副作用。
const addItemToCart = (cart, item) => {
cart.push({ item, date: Date.now() });
};
const addItemToCart = (cart, item) => {
return [...cart, { item, date: Date.now() }];
};
8. 避免全局函数 ❌
污染 global 是一种不好的做法,因为你可能会与另一个库发生冲突,在开发中遇到意想不到的结果。
例如:你想扩展 JS 中的 Array,为其添加一个 diff 函数显示两个数组间的差异,此时应如何去做?你可以将 diff 写入 Array.prototype,但这么做会和其他有类似需求的库造成冲突。如果另一个库对 diff 的需求为比较一个数组中首尾元素间的差异呢?
使用 ES6 中的 class 对全局的 Array 做简单的扩展显然是一个更棒的选择。
Array.prototype.diff = function(comparisonArray) {
const hash = new Set(comparisonArray);
return this.filter(item => !hash.has(item))
}
class SuperArray extends Array {
constructor(...args) {
super(...args);
}
diff(comparisonArray) {
const hash = new Set(comparisonArray);
return this.filter(item => !hash.has(item))
}
}
9. 函数式编程 ❌
函数式的编程 接受一个值并返回一个结果, 让函数更干净且易于测试
const programmerOutput = [
{
name: "Uncle Bobby",
linesOfCode: 500
},
{
name: "Suzie Q",
linesOfCode: 1500
},
{
name: "Jimmy Gosling",
linesOfCode: 150
},
{
name: "Gracie Hopper",
linesOfCode: 1000
}
];
let totalOutput = 0;
for (let i = 0; i < programmerOutput.length; i++) {
totalOutput += programmerOutput[i].linesOfCode;
}
const programmerOutput = [
{
name: "Uncle Bobby",
linesOfCode: 500
},
{
name: "Suzie Q",
linesOfCode: 1500
},
{
name: "Jimmy Gosling",
linesOfCode: 150
},
{
name: "Gracie Hopper",
linesOfCode: 1000
}
];
const totalOutput = programmerOutput.reduce(
(totalLines, output) => totalLines + output.linesOfCode,
0
);
三、对象和数据结构(Objects and Data Structures)
1. Getter & Setter ✅
使用 getter 和 setter 来访问对象上的数据可能比简单地查找对象上的属性要好:
- get 统一处理返回结果,调整更方便。
- set 值时,可以方便添加验证。
- 内部封装功能实现。
- 当 get 或 set 时,容易捕获日志和错误。
- 可实现异步获取。
function makeBankAccount() {
// ...
return {
balance: 0
// ...
};
}
const account = makeBankAccount();
account.balance = 100;
function makeBankAccount() {
// this one is private
let balance = 0;
// a "getter", made public via the returned object below
function getBalance() {
return balance;
}
// a "setter", made public via the returned object below
function setBalance(amount) {
// ... validate before updating the balance
balance = amount;
}
return {
// ...
getBalance,
setBalance
};
}
const account = makeBankAccount();
account.setBalance(100);
2. 私有化对象成员 ✅
可以通过闭包实现
const Employee = function(name) {
this.name = name;
};
Employee.prototype.getName = function getName() {
return this.name;
};
const employee = new Employee("John Doe");
console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe
delete employee.name;
console.log(`Employee name: ${employee.getName()}`); // Employee name: undefined
function makeEmployee(name) {
return {
getName() {
return name;
}
};
}
const employee = makeEmployee("John Doe");
console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe
delete employee.name;
console.log(`Employee name: ${employee.getName()}`); // Employee name: John Doe
四、类(Classes)
1. 使用 ES6 的类代替 ES5 的构造函数✅
ES6的类 让对象原型的写法更加清晰、更像面向对象编程的语法
const Animal = function(age) {
if (!(this instanceof Animal)) {
throw new Error("Instantiate Animal with `new`");
}
this.age = age;
};
Animal.prototype.move = function move() {};
const Mammal = function(age, furColor) {
if (!(this instanceof Mammal)) {
throw new Error("Instantiate Mammal with `new`");
}
Animal.call(this, age);
this.furColor = furColor;
};
Mammal.prototype = Object.create(Animal.prototype);
Mammal.prototype.constructor = Mammal;
Mammal.prototype.liveBirth = function liveBirth() {};
const Human = function(age, furColor, languageSpoken) {
if (!(this instanceof Human)) {
throw new Error("Instantiate Human with `new`");
}
Mammal.call(this, age, furColor);
this.languageSpoken = languageSpoken;
};
Human.prototype = Object.create(Mammal.prototype);
Human.prototype.constructor = Human;
Human.prototype.speak = function speak() {};
class Animal {
constructor(age) {
this.age = age;
}
move() {
/* ... */
}
}
class Mammal extends Animal {
constructor(age, furColor) {
super(age);
this.furColor = furColor;
}
liveBirth() {
/* ... */
}
}
class Human extends Mammal {
constructor(age, furColor, languageSpoken) {
super(age, furColor);
this.languageSpoken = languageSpoken;
}
speak() {
/* ... */
}
}
2. 链式方法✅
链式方法让代码不冗余,更有表现力。有争论说方法链不够干净且违反了德米特法则。但这种方法在 JS 及许多库(如 JQuery、Moment)中显得非常实用。
在 class 的函数中返回 this,能够方便的将类需要执行的多个方法链接起来。
class Car {
constructor(make, model, color) {
this.make = make;
this.model = model;
this.color = color;
}
setMake(make) {
this.make = make;
}
setModel(model) {
this.model = model;
}
setColor(color) {
this.color = color;
}
save() {
console.log(this.make, this.model, this.color);
}
}
const car = new Car("Ford", "F-150", "red");
car.setColor("pink");
car.save();
class Car {
constructor(make, model, color) {
this.make = make;
this.model = model;
this.color = color;
}
setMake(make) {
this.make = make;
// NOTE: Returning this for chaining
return this;
}
setModel(model) {
this.model = model;
// NOTE: Returning this for chaining
return this;
}
setColor(color) {
this.color = color;
// NOTE: Returning this for chaining
return this;
}
save() {
console.log(this.make, this.model, this.color);
// NOTE: Returning this for chaining
return this;
}
}
const car = new Car("Ford", "F-150", "red").setColor("pink").save();
3. 组合优于继承✅
继承优于组合的场景:
- 你的继承表示 is-a 关系,而不是 has-a 关系 (Human->Animal vs User->UserDetails)。
- 你可以重用基类代码(人可以像所有动物一样移动)。
- 要通过更改基类对派生类进行全局更改(改变所有动物运动时的热量消耗)。
class Employee {
constructor(name, email) {
this.name = name;
this.email = email;
}
// ...
}
// Bad because Employees "have" tax data. EmployeeTaxData is not a type of Employee
class EmployeeTaxData extends Employee {
constructor(ssn, salary) {
super();
this.ssn = ssn;
this.salary = salary;
}
// ...
}
class EmployeeTaxData {
constructor(ssn, salary) {
this.ssn = ssn;
this.salary = salary;
}
// ...
}
class Employee {
constructor(name, email) {
this.name = name;
this.email = email;
}
setTaxData(ssn, salary) {
this.taxData = new EmployeeTaxData(ssn, salary);
}
// ...
}
五、面向对象设计(SOLID)
1、单一责任原则 Single Responsibility Principle (SRP)
上面函数有提到过,一个类具有太多太杂的功能,就需要拆分,一个类只做一件事
class UserSettings {
constructor(user) {
this.user = user;
}
changeSettings(settings) {
if (this.verifyCredentials()) {
// ...
}
}
verifyCredentials() {
// ...
}
}
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()) {
// ...
}
}
}
2、开闭原则 Open/Closed Principle (OCP)
应该对外扩展开放,但对内修改关闭。通俗的讲就是只允许用户在不更改现有代码的情况下添加新功能。
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
}
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
});
}
}
3、里氏替换原则 Liskov Substitution Principle (LSP)
派生类(子类)中的对象可以在程序中代替其基类(超类)中的对象。
如果你有一个父类和一个子类,那么父类和子类的同一功能调用,结果一致。这可能令人困惑,所以让我们看一下经典的正方形-矩形例子。从数学上讲,一个正方形就是一个矩形,但是如果你通过继承使用 is-a 关系来建模它,你会发现这是错误的。
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);
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);
4、接口隔离原则 Interface Segregation Principle (ISP)
JavaScript 没有接口,所以这个原则不像其他原则那么严格。然而,即使 JavaScript 缺少类型系统,这一点也很重要。
ISP声明:“不应该强迫客户依赖他们不使用的接口。”由于duck typing的存在,接口在JavaScript中是隐式契约。
在JavaScript中演示这一原则的一个很好的例子是需要大型设置对象的类。不要求客户端设置大量的选项是有益的,因为大多数时候他们不需要所有的设置。让它们成为可选的有助于避免“胖接口”。
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.
// ...
});
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() {}
}
});
5、依赖反转原则 Dependency Inversion Principle (DIP)
该原则有两个核心点:
- 高层模块不应该依赖于低层模块。他们都应该依赖于抽象接口。
- 抽象接口应该脱离具体实现,具体实现应该依赖于抽象接口。
AngularJS,使用依赖注入(Dependency Injection, DI)的形式实现了这个原则。尽管它们不是相同的概念,DIP 防止高级模块了解其低级模块的实现并设置它们。它可以通过 DI 实现这一点。这样做的一个巨大好处是它减少了模块之间的耦合。耦合是一种非常糟糕的开发模式,因为它使代码难以重构。
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();
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();