1704 lines
34 KiB
Markdown
1704 lines
34 KiB
Markdown
---
|
||
nav:
|
||
title: 前端
|
||
path: /fea
|
||
group:
|
||
title: 💊 书写规范
|
||
order: 4
|
||
path: /norms
|
||
---
|
||
|
||
## 💊 书写规范
|
||
|
||
### 使用语义化的命名
|
||
|
||
**Bad:**
|
||
|
||
```js
|
||
const yyyymmdstr = moment().format("YYYY/MM/DD");
|
||
```
|
||
|
||
**Good:**
|
||
|
||
```js
|
||
const currentDate = moment().format("YYYY/MM/DD");
|
||
```
|
||
|
||
### 对相同类型的变量使用相同的词汇表
|
||
|
||
**Bad:**
|
||
|
||
```javascript
|
||
getUserInfo();
|
||
getClientData();
|
||
getCustomerRecord();
|
||
```
|
||
|
||
**Good:**
|
||
|
||
```javascript
|
||
getUser();
|
||
```
|
||
|
||
### 静态变量用全大写定义好
|
||
|
||
**Bad:**
|
||
|
||
```javascript
|
||
// 86400000 是啥意思?
|
||
setTimeout(blastOff, 86400000);
|
||
```
|
||
|
||
**Good:**
|
||
|
||
```javascript
|
||
// 说明这个时间的定义原因
|
||
const MILLISECONDS_PER_DAY = 60 * 60 * 24 * 1000; //86400000;
|
||
|
||
setTimeout(blastOff, MILLISECONDS_PER_DAY);
|
||
```
|
||
|
||
## **函数**
|
||
|
||
### 函数名应该说明它们的作用
|
||
|
||
**Bad:**
|
||
|
||
```javascript
|
||
function addToDate(date, month) {
|
||
// ...
|
||
}
|
||
|
||
const date = new Date();
|
||
|
||
// It's hard to tell from the function name what is added
|
||
addToDate(date, 1);
|
||
```
|
||
|
||
**Good:**
|
||
|
||
```javascript
|
||
function addMonthToDate(month, date) {
|
||
// ...
|
||
}
|
||
|
||
const date = new Date();
|
||
addMonthToDate(1, date);
|
||
```
|
||
|
||
|
||
### 函数应该只是一个抽象层次
|
||
|
||
当你有一个以上的抽象层次时,你的函数通常做得太多了。拆分功能可以带来可重用性和更容易的测试。
|
||
|
||
**Bad:**
|
||
|
||
```javascript
|
||
function parseBetterJSAlternative(code) {
|
||
const REGEXES = [
|
||
// ...
|
||
];
|
||
|
||
const statements = code.split(" ");
|
||
const tokens = [];
|
||
REGEXES.forEach(REGEX => {
|
||
statements.forEach(statement => {
|
||
// ...
|
||
});
|
||
});
|
||
|
||
const ast = [];
|
||
tokens.forEach(token => {
|
||
// lex...
|
||
});
|
||
|
||
ast.forEach(node => {
|
||
// parse...
|
||
});
|
||
}
|
||
```
|
||
|
||
**Good:**
|
||
|
||
```javascript
|
||
function parseBetterJSAlternative(code) {
|
||
const tokens = tokenize(code);
|
||
const syntaxTree = parse(tokens);
|
||
syntaxTree.forEach(node => {
|
||
// parse...
|
||
});
|
||
}
|
||
|
||
function tokenize(code) {
|
||
const REGEXES = [
|
||
// ...
|
||
];
|
||
|
||
const statements = code.split(" ");
|
||
const tokens = [];
|
||
REGEXES.forEach(REGEX => {
|
||
statements.forEach(statement => {
|
||
tokens.push(/* ... */);
|
||
});
|
||
});
|
||
|
||
return tokens;
|
||
}
|
||
|
||
function parse(tokens) {
|
||
const syntaxTree = [];
|
||
tokens.forEach(token => {
|
||
syntaxTree.push(/* ... */);
|
||
});
|
||
|
||
return syntaxTree;
|
||
}
|
||
```
|
||
|
||
### 删除重复的代码
|
||
|
||
尽最大努力避免重复代码。 重复代码是不好的,因为这意味着如果您需要更改某些逻辑,有不止一个地方可以更改某些内容。
|
||
|
||
想象一下,如果你经营一家餐厅并跟踪您的库存:所有的西红柿、洋葱、大蒜、香料等。如果有多个清单,那么当提供带有西红柿的菜肴时,所有清单都必须更新一遍。 但如果只有一个列表,只需要更新一个地方。
|
||
|
||
**Bad:**
|
||
|
||
```javascript
|
||
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);
|
||
});
|
||
}
|
||
```
|
||
|
||
**Good:**
|
||
|
||
```javascript
|
||
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);
|
||
});
|
||
}
|
||
```
|
||
|
||
### 使用Object.assign设置默认对象
|
||
|
||
**Bad:**
|
||
|
||
```javascript
|
||
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);
|
||
```
|
||
|
||
**Good:**
|
||
|
||
```javascript
|
||
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
|
||
// config now equals: {title: "Order", body: "Bar", buttonText: "Send", cancellable: true}
|
||
// ...
|
||
}
|
||
|
||
createMenu(menuConfig);
|
||
```
|
||
|
||
### 不要使用标志作为函数参数
|
||
|
||
标志告诉用户这个函数做不止一件事。函数应该只做一件事。如果函数基于布尔值执行不同的代码逻辑,则应该将它们分割开来。
|
||
|
||
**Bad:**
|
||
|
||
```javascript
|
||
function createFile(name, temp) {
|
||
if (temp) {
|
||
fs.create(`./temp/${name}`);
|
||
} else {
|
||
fs.create(name);
|
||
}
|
||
}
|
||
```
|
||
|
||
**Good:**
|
||
|
||
```javascript
|
||
function createFile(name) {
|
||
fs.create(name);
|
||
}
|
||
|
||
function createTempFile(name) {
|
||
createFile(`./temp/${name}`);
|
||
}
|
||
```
|
||
|
||
### 避免副作用(1)
|
||
|
||
**Bad:**
|
||
|
||
```javascript
|
||
let name = "Ryan McDermott";
|
||
|
||
function splitIntoFirstAndLastName() {
|
||
name = name.split(" ");
|
||
}
|
||
|
||
splitIntoFirstAndLastName();
|
||
|
||
console.log(name); // ['Ryan', 'McDermott'];
|
||
```
|
||
|
||
**Good:**
|
||
|
||
```javascript
|
||
function splitIntoFirstAndLastName(name) {
|
||
return name.split(" ");
|
||
}
|
||
|
||
const name = "Ryan McDermott";
|
||
const newName = splitIntoFirstAndLastName(name);
|
||
|
||
console.log(name); // 'Ryan McDermott';
|
||
console.log(newName); // ['Ryan', 'McDermott'];
|
||
```
|
||
|
||
### 避免副作用(2)
|
||
|
||
**Bad:**
|
||
|
||
```javascript
|
||
const addItemToCart = (cart, item) => {
|
||
cart.push({ item, date: Date.now() });
|
||
};
|
||
```
|
||
|
||
**Good:**
|
||
|
||
```javascript
|
||
const addItemToCart = (cart, item) => {
|
||
return [...cart, { item, date: Date.now() }];
|
||
};
|
||
```
|
||
|
||
### 不要更改全局函数
|
||
|
||
**Bad:**
|
||
|
||
```javascript
|
||
Array.prototype.diff = function diff(comparisonArray) {
|
||
const hash = new Set(comparisonArray);
|
||
return this.filter(elem => !hash.has(elem));
|
||
};
|
||
```
|
||
|
||
**Good:**
|
||
|
||
```javascript
|
||
class SuperArray extends Array {
|
||
diff(comparisonArray) {
|
||
const hash = new Set(comparisonArray);
|
||
return this.filter(elem => !hash.has(elem));
|
||
}
|
||
}
|
||
```
|
||
|
||
### 多用函数式编程
|
||
|
||
JavaScript不像Haskell那样是一种函数式语言,但它具有函数式风格。函数式语言更简洁,更容易测试。如果可以的话,尽量使用这种编程风格。
|
||
|
||
**Bad:**
|
||
|
||
```javascript
|
||
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;
|
||
}
|
||
```
|
||
|
||
**Good:**
|
||
|
||
```javascript
|
||
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
|
||
);
|
||
```
|
||
|
||
### 封装条件
|
||
|
||
**Bad:**
|
||
|
||
```javascript
|
||
if (fsm.state === "fetching" && isEmpty(listNode)) {
|
||
// ...
|
||
}
|
||
```
|
||
|
||
**Good:**
|
||
|
||
```javascript
|
||
function shouldShowSpinner(fsm, listNode) {
|
||
return fsm.state === "fetching" && isEmpty(listNode);
|
||
}
|
||
|
||
if (shouldShowSpinner(fsmInstance, listNodeInstance)) {
|
||
// ...
|
||
}
|
||
```
|
||
|
||
### 避免条件
|
||
|
||
这似乎是一个不可能完成的任务。大多数人一听到这句话就会说:“如果没有if语句,我该怎么做呢?”答案是,您可以在许多情况下使用多态性来实现相同的任务。第二个问题通常是,"这很好,但我为什么要这么做?"答案是我们以前学到的一个干净的代码概念:一个函数应该只做一件事。当你有带有if语句的类和函数时,你是在告诉用户你的函数做不止一件事。记住,只做一件事。
|
||
|
||
**Bad:**
|
||
|
||
```javascript
|
||
class Airplane {
|
||
// ...
|
||
getCruisingAltitude() {
|
||
switch (this.type) {
|
||
case "777":
|
||
return this.getMaxAltitude() - this.getPassengerCount();
|
||
case "Air Force One":
|
||
return this.getMaxAltitude();
|
||
case "Cessna":
|
||
return this.getMaxAltitude() - this.getFuelExpenditure();
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
**Good:**
|
||
|
||
```javascript
|
||
class Airplane {
|
||
// ...
|
||
}
|
||
|
||
class Boeing777 extends Airplane {
|
||
// ...
|
||
getCruisingAltitude() {
|
||
return this.getMaxAltitude() - this.getPassengerCount();
|
||
}
|
||
}
|
||
|
||
class AirForceOne extends Airplane {
|
||
// ...
|
||
getCruisingAltitude() {
|
||
return this.getMaxAltitude();
|
||
}
|
||
}
|
||
|
||
class Cessna extends Airplane {
|
||
// ...
|
||
getCruisingAltitude() {
|
||
return this.getMaxAltitude() - this.getFuelExpenditure();
|
||
}
|
||
}
|
||
```
|
||
|
||
### 避免类型检查 (part 1)
|
||
|
||
JavaScript是无类型的,这意味着函数可以接受任何类型的参数。有时,您会被这种自由所困扰,并忍不住在函数中进行类型检查。有很多方法可以避免这样做。首先要考虑的是一致的api。
|
||
|
||
**Bad:**
|
||
|
||
```javascript
|
||
function travelToTexas(vehicle) {
|
||
if (vehicle instanceof Bicycle) {
|
||
vehicle.pedal(this.currentLocation, new Location("texas"));
|
||
} else if (vehicle instanceof Car) {
|
||
vehicle.drive(this.currentLocation, new Location("texas"));
|
||
}
|
||
}
|
||
```
|
||
|
||
**Good:**
|
||
|
||
```javascript
|
||
function travelToTexas(vehicle) {
|
||
vehicle.move(this.currentLocation, new Location("texas"));
|
||
}
|
||
```
|
||
|
||
### 避免类型检查 (part 2)
|
||
|
||
如果你使用的是基本的原始值,比如字符串和整数,你不能使用多态性,但你仍然觉得有必要进行类型检查,你应该考虑使用TypeScript。它是普通JavaScript的一个很好的替代方案,因为它提供了标准JavaScript语法之上的静态类型。手动类型检查常规JavaScript的问题是,要想做得好,需要太多额外的废话,以至于你得到的伪“类型安全”并不能弥补失去的可读性。保持JavaScript整洁,编写优秀的测试,并进行良好的代码审查。
|
||
**Bad:**
|
||
|
||
```javascript
|
||
function combine(val1, val2) {
|
||
if (
|
||
(typeof val1 === "number" && typeof val2 === "number") ||
|
||
(typeof val1 === "string" && typeof val2 === "string")
|
||
) {
|
||
return val1 + val2;
|
||
}
|
||
|
||
throw new Error("Must be of type String or Number");
|
||
}
|
||
```
|
||
|
||
**Good:**
|
||
|
||
```javascript
|
||
function combine(val1, val2) {
|
||
return val1 + val2;
|
||
}
|
||
```
|
||
|
||
### 不要过度优化
|
||
|
||
**Bad:**
|
||
|
||
```javascript
|
||
for (let i = 0, len = list.length; i < len; i++) {
|
||
// ...
|
||
}
|
||
```
|
||
|
||
**Good:**
|
||
|
||
```javascript
|
||
for (let i = 0; i < list.length; i++) {
|
||
// ...
|
||
}
|
||
```
|
||
|
||
### 删除无用代码
|
||
|
||
死代码和重复代码一样糟糕。没有理由将它保存在代码库中。如果它没有被调用,就把它处理掉!如果您仍然需要它,它在您的版本历史中仍然是安全的。
|
||
|
||
**Bad:**
|
||
|
||
```javascript
|
||
function oldRequestModule(url) {
|
||
// ...
|
||
}
|
||
|
||
function newRequestModule(url) {
|
||
// ...
|
||
}
|
||
|
||
const req = newRequestModule;
|
||
inventoryTracker("apples", req, "www.inventory-awesome.io");
|
||
```
|
||
|
||
**Good:**
|
||
|
||
```javascript
|
||
function newRequestModule(url) {
|
||
// ...
|
||
}
|
||
|
||
const req = newRequestModule;
|
||
inventoryTracker("apples", req, "www.inventory-awesome.io");
|
||
```
|
||
|
||
## **对象和数据结构**
|
||
|
||
### 使用getters/setters语句
|
||
|
||
使用getter和setter访问对象上的数据可能比简单地查找对象上的属性更好。你可能会问:“为什么?“,下面列举了一些原因:
|
||
|
||
- 当您想要做的不仅仅是获取一个对象属性时,您不必在代码库中查找和更改每个访问器。
|
||
- 使得`set`执行集合时添加验证变得简单。
|
||
- 易于添加日志和错误处理时,获取和设置。
|
||
- 你可以延迟加载对象的属性,比如说从服务器获取。
|
||
|
||
**Bad:**
|
||
|
||
```javascript
|
||
function makeBankAccount() {
|
||
// ...
|
||
|
||
return {
|
||
balance: 0
|
||
// ...
|
||
};
|
||
}
|
||
|
||
const account = makeBankAccount();
|
||
account.balance = 100;
|
||
```
|
||
|
||
**Good:**
|
||
|
||
```javascript
|
||
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);
|
||
```
|
||
|
||
### 使对象具有私有成员
|
||
|
||
这可以通过闭包(针对ES5及以下版本)来实现。
|
||
|
||
**Bad:**
|
||
|
||
```javascript
|
||
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
|
||
```
|
||
|
||
**Good:**
|
||
|
||
```javascript
|
||
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**
|
||
|
||
### 多用ES6
|
||
|
||
**Bad:**
|
||
|
||
```javascript
|
||
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() {};
|
||
```
|
||
|
||
**Good:**
|
||
|
||
```javascript
|
||
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() {
|
||
/* ... */
|
||
}
|
||
}
|
||
```
|
||
|
||
### 使用方法链
|
||
|
||
这种模式在 JavaScript 中非常有用,您可以在许多库中看到它,例如 jQuery 和 Lodash。 它使您的代码具有表现力,并且不那么冗长。 出于这个原因,我说,使用方法链并看看您的代码将有多干净。 在您的类函数中,只需在每个函数的末尾返回 this,您就可以将更多的类方法链接到它上面。
|
||
|
||
**Bad:**
|
||
|
||
```javascript
|
||
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();
|
||
```
|
||
|
||
**Good:**
|
||
|
||
```javascript
|
||
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();
|
||
```
|
||
|
||
### 偏好组合而非继承
|
||
|
||
正如在Gang of Four的设计模式中所说的那样,您应该尽可能地选择组合而不是继承。有很多很好的理由使用继承,也有很多很好的理由使用组合。这条格言的主要观点是,如果你的头脑本能地倾向于继承,试着思考一下构图是否能更好地模拟你的问题。在某些情况下是可以的。
|
||
|
||
您可能会想,“我应该在什么时候使用继承?”这取决于您手头的问题,但这是继承比组合更有意义的一些不错的点:
|
||
|
||
1. 你的继承代表的是“是-a”关系而不是“是-a”关系(Human->Animal vs. User->UserDetails)。
|
||
relationship (Human->Animal vs. User->UserDetails).
|
||
2. 您可以重用基类中的代码(人类可以像所有动物一样移动)。
|
||
3. 您希望通过更改基类来对派生类进行全局更改。(改变所有动物运动时的热量消耗)。
|
||
|
||
**Bad:**
|
||
|
||
```javascript
|
||
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;
|
||
}
|
||
|
||
// ...
|
||
}
|
||
```
|
||
|
||
**Good:**
|
||
|
||
```javascript
|
||
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);
|
||
}
|
||
// ...
|
||
}
|
||
```
|
||
|
||
## **固态**
|
||
|
||
### 单一责任原则
|
||
|
||
正如Clean Code中所述,“一个类的更改原因不应该超过一个”。如果你只能带一个行李箱上飞机,那就很容易把一门课程塞进很多功能中。这样做的问题是,你的类在概念上没有连贯性,这会给它带来很多改变的理由。将更改类的时间最小化是很重要的。这很重要,因为如果一个类中有太多的功能,而您修改了其中的一部分,就很难理解这将如何影响代码库中的其他依赖模块。
|
||
|
||
**Bad:**
|
||
|
||
```javascript
|
||
class UserSettings {
|
||
constructor(user) {
|
||
this.user = user;
|
||
}
|
||
|
||
changeSettings(settings) {
|
||
if (this.verifyCredentials()) {
|
||
// ...
|
||
}
|
||
}
|
||
|
||
verifyCredentials() {
|
||
// ...
|
||
}
|
||
}
|
||
```
|
||
|
||
**Good:**
|
||
|
||
```javascript
|
||
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()) {
|
||
// ...
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### 开放封闭原则
|
||
|
||
正如Bertrand Meyer所说,“软件实体(类、模块、函数等)应该对扩展开放,但对修改关闭。”这意味着什么呢?这个原则基本上是说,您应该允许用户添加新功能,而不改变现有的代码。
|
||
|
||
**Bad:**
|
||
|
||
```javascript
|
||
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:**
|
||
|
||
```javascript
|
||
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
|
||
});
|
||
}
|
||
}
|
||
```
|
||
|
||
### 里氏替换原则
|
||
|
||
对于一个非常简单的概念来说,这是一个可怕的术语。它被正式定义为“如果S是T的子类型,那么T类型的对象可以被S类型的对象替换(也就是说,S类型的对象可以替换T类型的对象),而不改变该程序的任何理想属性(正确性,执行的任务,等等)。”这是一个更可怕的定义。
|
||
|
||
对此最好的解释是,如果你有一个父类和一个子类,那么基类和子类可以互换使用,而不会得到错误的结果。这可能仍然令人困惑,所以让我们看看经典的正方形-矩形示例。从数学上讲,正方形是一个矩形,但是如果您通过继承使用“is-a”关系来建模它,您很快就会遇到麻烦。
|
||
|
||
**Bad:**
|
||
|
||
```javascript
|
||
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:**
|
||
|
||
```javascript
|
||
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声明:“不应该强迫客户依赖他们不使用的接口。”由于duck类型,接口在JavaScript中是隐式契约
|
||
|
||
在JavaScript中演示这一原则的一个很好的例子是需要大型设置对象的类。不需要客户设置大量的选项是有益的,因为大多数时候他们不需要所有的设置。使它们可选有助于防止出现“臃肿”。
|
||
|
||
**Bad:**
|
||
|
||
```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.
|
||
// ...
|
||
});
|
||
```
|
||
|
||
**Good:**
|
||
|
||
```javascript
|
||
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() {}
|
||
}
|
||
});
|
||
```
|
||
|
||
### 依赖倒置原理
|
||
|
||
这个原则陈述了两个重要的事情:
|
||
|
||
1. 高级模块不应该依赖于低级模块。 两者都应该依赖于抽象。
|
||
2. 抽象不应该依赖于细节。 细节应该取决于抽象。
|
||
|
||
一开始这可能很难理解,但是如果您使用过 AngularJS,您就会看到该原则以依赖注入 (DI) 的形式实现。 虽然它们不是相同的概念,但 DIP 使高级模块无法了解其低级模块的详细信息并进行设置。 它可以通过 DI 实现这一点。 这样做的一个巨大好处是它减少了模块之间的耦合。 耦合是一种非常糟糕的开发模式,因为它使您的代码难以重构。
|
||
|
||
如前所述,JavaScript 没有接口,因此依赖的抽象是隐式契约。 也就是说,一个对象/类向另一个对象/类公开的方法和属性。 在下面的示例中,隐式约定是 `anInventoryTracker` 的任何请求模块都将具有 `requestItems` 方法。
|
||
|
||
**Bad:**
|
||
|
||
```javascript
|
||
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:**
|
||
|
||
```javascript
|
||
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();
|
||
```
|
||
|
||
## **测试**
|
||
|
||
### 每次测试单一概念
|
||
|
||
**Bad:**
|
||
|
||
```javascript
|
||
import assert from "assert";
|
||
|
||
describe("MomentJS", () => {
|
||
it("handles date boundaries", () => {
|
||
let date;
|
||
|
||
date = new MomentJS("1/1/2015");
|
||
date.addDays(30);
|
||
assert.equal("1/31/2015", date);
|
||
|
||
date = new MomentJS("2/1/2016");
|
||
date.addDays(28);
|
||
assert.equal("02/29/2016", date);
|
||
|
||
date = new MomentJS("2/1/2015");
|
||
date.addDays(28);
|
||
assert.equal("03/01/2015", date);
|
||
});
|
||
});
|
||
```
|
||
|
||
**Good:**
|
||
|
||
```javascript
|
||
import assert from "assert";
|
||
|
||
describe("MomentJS", () => {
|
||
it("handles 30-day months", () => {
|
||
const date = new MomentJS("1/1/2015");
|
||
date.addDays(30);
|
||
assert.equal("1/31/2015", date);
|
||
});
|
||
|
||
it("handles leap year", () => {
|
||
const date = new MomentJS("2/1/2016");
|
||
date.addDays(28);
|
||
assert.equal("02/29/2016", date);
|
||
});
|
||
|
||
it("handles non-leap year", () => {
|
||
const date = new MomentJS("2/1/2015");
|
||
date.addDays(28);
|
||
assert.equal("03/01/2015", date);
|
||
});
|
||
});
|
||
```
|
||
|
||
## **并发性**
|
||
|
||
### 尽量使用promise
|
||
|
||
Callbacks aren't clean, and they cause excessive amounts of nesting. With ES2015/ES6,
|
||
Promises are a built-in global type. Use them!
|
||
|
||
**Bad:**
|
||
|
||
```javascript
|
||
import { get } from "request";
|
||
import { writeFile } from "fs";
|
||
|
||
get(
|
||
"https://en.wikipedia.org/wiki/Robert_Cecil_Martin",
|
||
(requestErr, response, body) => {
|
||
if (requestErr) {
|
||
console.error(requestErr);
|
||
} else {
|
||
writeFile("article.html", body, writeErr => {
|
||
if (writeErr) {
|
||
console.error(writeErr);
|
||
} else {
|
||
console.log("File written");
|
||
}
|
||
});
|
||
}
|
||
}
|
||
);
|
||
```
|
||
|
||
**Good:**
|
||
|
||
```javascript
|
||
import { get } from "request-promise";
|
||
import { writeFile } from "fs-extra";
|
||
|
||
get("https://en.wikipedia.org/wiki/Robert_Cecil_Martin")
|
||
.then(body => {
|
||
return writeFile("article.html", body);
|
||
})
|
||
.then(() => {
|
||
console.log("File written");
|
||
})
|
||
.catch(err => {
|
||
console.error(err);
|
||
});
|
||
```
|
||
|
||
### Async/Await 更优于Promise
|
||
|
||
Promise 是回调的一个非常干净的替代方案,但 `ES2017/ES8` 带来了 `async` 和 `await` ,它们提供了一个更干净的解决方案。 您所需要的只是一个以 async 关键字为前缀的函数,然后您就可以在没有 then 函数链的情况下命令式地编写逻辑。 如果您今天可以利用 `ES2017/ES8` 功能,请使用它!
|
||
|
||
|
||
**Bad:**
|
||
|
||
```javascript
|
||
import { get } from "request-promise";
|
||
import { writeFile } from "fs-extra";
|
||
|
||
get("https://en.wikipedia.org/wiki/Robert_Cecil_Martin")
|
||
.then(body => {
|
||
return writeFile("article.html", body);
|
||
})
|
||
.then(() => {
|
||
console.log("File written");
|
||
})
|
||
.catch(err => {
|
||
console.error(err);
|
||
});
|
||
```
|
||
|
||
**Good:**
|
||
|
||
```javascript
|
||
import { get } from "request-promise";
|
||
import { writeFile } from "fs-extra";
|
||
|
||
async function getCleanCodeArticle() {
|
||
try {
|
||
const body = await get(
|
||
"https://en.wikipedia.org/wiki/Robert_Cecil_Martin"
|
||
);
|
||
await writeFile("article.html", body);
|
||
console.log("File written");
|
||
} catch (err) {
|
||
console.error(err);
|
||
}
|
||
}
|
||
|
||
getCleanCodeArticle()
|
||
```
|
||
|
||
## **错误警告捕捉**
|
||
|
||
### 不要忘记捕捉报错
|
||
|
||
对捕获的错误不采取任何措施并不能让您永远修复或应对所述错误。 将错误记录到控制台 (`console.log`) 并没有好到哪里去,因为它经常会迷失在打印到控制台的大量内容中。 如果你在 `try/catch` 中包装了任何代码,这意味着你认为那里可能会发生错误,因此你应该有一个计划,或者创建一个代码路径,以备不时之需。
|
||
|
||
**Bad:**
|
||
|
||
```javascript
|
||
try {
|
||
functionThatMightThrow();
|
||
} catch (error) {
|
||
console.log(error);
|
||
}
|
||
```
|
||
|
||
**Good:**
|
||
|
||
```javascript
|
||
try {
|
||
functionThatMightThrow();
|
||
} catch (error) {
|
||
// One option (more noisy than console.log):
|
||
console.error(error);
|
||
// Another option:
|
||
notifyUserOfError(error);
|
||
// Another option:
|
||
reportErrorToService(error);
|
||
// OR do all three!
|
||
}
|
||
```
|
||
|
||
## **模式**
|
||
|
||
### 一致化命名
|
||
|
||
JavaScript是无类型的,所以大写可以告诉你很多关于变量、函数等的信息。这些规则是主观的,所以你的团队可以选择他们想要的任何规则。关键是,无论你们选择什么,保持一致就好。
|
||
|
||
**Bad:**
|
||
|
||
```javascript
|
||
const DAYS_IN_WEEK = 7;
|
||
const daysInMonth = 30;
|
||
|
||
const songs = ["Back In Black", "Stairway to Heaven", "Hey Jude"];
|
||
const Artists = ["ACDC", "Led Zeppelin", "The Beatles"];
|
||
|
||
function eraseDatabase() {}
|
||
function restore_database() {}
|
||
|
||
class animal {}
|
||
class Alpaca {}
|
||
```
|
||
|
||
**Good:**
|
||
|
||
```javascript
|
||
const DAYS_IN_WEEK = 7;
|
||
const DAYS_IN_MONTH = 30;
|
||
|
||
const SONGS = ["Back In Black", "Stairway to Heaven", "Hey Jude"];
|
||
const ARTISTS = ["ACDC", "Led Zeppelin", "The Beatles"];
|
||
|
||
function eraseDatabase() {}
|
||
function restoreDatabase() {}
|
||
|
||
class Animal {}
|
||
class Alpaca {}
|
||
```
|
||
|
||
### 函数调用者和被调用者应该是接近的
|
||
|
||
如果一个函数调用另一个函数,在源文件中保持这些函数垂直接近。理想情况下,让打电话的人在被打电话的人的正上方。我们倾向于从上到下阅读代码,就像阅读报纸一样。因此,让您的代码以这种方式阅读。
|
||
|
||
**Bad:**
|
||
|
||
```javascript
|
||
class PerformanceReview {
|
||
constructor(employee) {
|
||
this.employee = employee;
|
||
}
|
||
|
||
lookupPeers() {
|
||
return db.lookup(this.employee, "peers");
|
||
}
|
||
|
||
lookupManager() {
|
||
return db.lookup(this.employee, "manager");
|
||
}
|
||
|
||
getPeerReviews() {
|
||
const peers = this.lookupPeers();
|
||
// ...
|
||
}
|
||
|
||
perfReview() {
|
||
this.getPeerReviews();
|
||
this.getManagerReview();
|
||
this.getSelfReview();
|
||
}
|
||
|
||
getManagerReview() {
|
||
const manager = this.lookupManager();
|
||
}
|
||
|
||
getSelfReview() {
|
||
// ...
|
||
}
|
||
}
|
||
|
||
const review = new PerformanceReview(employee);
|
||
review.perfReview();
|
||
```
|
||
|
||
**Good:**
|
||
|
||
```javascript
|
||
class PerformanceReview {
|
||
constructor(employee) {
|
||
this.employee = employee;
|
||
}
|
||
|
||
perfReview() {
|
||
this.getPeerReviews();
|
||
this.getManagerReview();
|
||
this.getSelfReview();
|
||
}
|
||
|
||
getPeerReviews() {
|
||
const peers = this.lookupPeers();
|
||
// ...
|
||
}
|
||
|
||
lookupPeers() {
|
||
return db.lookup(this.employee, "peers");
|
||
}
|
||
|
||
getManagerReview() {
|
||
const manager = this.lookupManager();
|
||
}
|
||
|
||
lookupManager() {
|
||
return db.lookup(this.employee, "manager");
|
||
}
|
||
|
||
getSelfReview() {
|
||
// ...
|
||
}
|
||
}
|
||
|
||
const review = new PerformanceReview(employee);
|
||
review.perfReview();
|
||
```
|
||
|
||
## **注释**
|
||
|
||
### 只注释具有业务逻辑复杂性的东西
|
||
|
||
**Bad:**
|
||
|
||
```javascript
|
||
function hashIt(data) {
|
||
// The hash
|
||
let hash = 0;
|
||
|
||
// Length of string
|
||
const length = data.length;
|
||
|
||
// Loop through every character in data
|
||
for (let i = 0; i < length; i++) {
|
||
// Get character code.
|
||
const char = data.charCodeAt(i);
|
||
// Make the hash
|
||
hash = (hash << 5) - hash + char;
|
||
// Convert to 32-bit integer
|
||
hash &= hash;
|
||
}
|
||
}
|
||
```
|
||
|
||
**Good:**
|
||
|
||
```javascript
|
||
function hashIt(data) {
|
||
let hash = 0;
|
||
const length = data.length;
|
||
|
||
for (let i = 0; i < length; i++) {
|
||
const char = data.charCodeAt(i);
|
||
hash = (hash << 5) - hash + char;
|
||
|
||
// Convert to 32-bit integer
|
||
hash &= hash;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 不要在你的代码库中留下注释掉的代码
|
||
|
||
**Bad:**
|
||
|
||
```javascript
|
||
doStuff();
|
||
// doOtherStuff();
|
||
// doSomeMoreStuff();
|
||
// doSoMuchStuff();
|
||
```
|
||
|
||
**Good:**
|
||
|
||
```javascript
|
||
doStuff();
|
||
```
|
||
|
||
### 不要写日志评论
|
||
|
||
记住,使用版本控制!不需要死代码、注释代码,尤其是日志注释。使用 `git log` 来获取历史记录
|
||
|
||
**Bad:**
|
||
|
||
```javascript
|
||
/**
|
||
* 2016-12-20: Removed monads, didn't understand them (RM)
|
||
* 2016-10-01: Improved using special monads (JP)
|
||
* 2016-02-03: Removed type-checking (LI)
|
||
* 2015-03-14: Added combine with type-checking (JR)
|
||
*/
|
||
function combine(a, b) {
|
||
return a + b;
|
||
}
|
||
```
|
||
|
||
**Good:**
|
||
|
||
```javascript
|
||
function combine(a, b) {
|
||
return a + b;
|
||
}
|
||
```
|