introduce
The author summarizes the software engineering principles applicable to JavaScript based on Robert C. Martin’s Code Cleanness Guide. Clean Code JavaScript
It is not necessary to adhere strictly to all the principles of this article, just a suggestion.
I. Variables
1. Use semantic variable names ✅
Small camel naming conventions: The way type + object is described
const yyyymmdstr = moment().format("YYYY/MM/DD");
Copy the code
const currentDate = moment().format("YYYY/MM/DD");
Copy the code
2. Const defines constant ✅
useconst
Declare a constant that should be immutable throughout the program.
var FIRST_US_PRESIDENT = "George Washington";
Copy the code
const FIRST_US_PRESIDENT = "George Washington";
Copy the code
3. Use the easily searchable name ✅
eliminateMagic string
(a specific string or number strongly coupled to the code), replaced by a variable with a clear meaning.
// What is 525600?
for (var i = 0; i < 525600; i++) {
runCronJob();
}
Copy the code
// Declare them as capitalized `var` globals.
const MINUTES_IN_A_YEAR = 525600;
for (var i = 0; i < MINUTES_IN_A_YEAR; i++) {
runCronJob();
}
Copy the code
4. Avoid repetitive descriptions ✅
Attribute names do not need to be repeated when class/object names already make sense.
var Car = {
carMake: 'Honda'.carModel: 'Accord'.carColor: 'Blue'
};
function paintCar(car) {
car.carColor = 'Red';
}
Copy the code
var Car = {
make: 'Honda'.model: 'Accord'.color: 'Blue'
};
function paintCar(car) {
car.color = 'Red';
}
Copy the code
5. Explanatory variables ✅
Attribute names do not need to be repeated when class/object names already make sense.
const cityStateRegex = /^(.+)[,\\s]+(.+?) \s*(\d{5})? $/;
// Do not know what the two parameters mean
saveCityState(
cityStateRegex.match(cityStateRegex)[1],
cityStateRegex.match(cityStateRegex)[2]);Copy the code
const address = "One Infinite Loop, Cupertino 95014";
const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?) \s*(\d{5})? $/;
const [_, city, zipCode] = address.match(cityZipCodeRegex) || [];
// Clearly understand the meaning of the two parameters
saveCityZipCode(city, zipCode);
Copy the code
Ii. Functions
1. Default parameter, instead of short-circuit evaluation ✅
If the default argument is used, the function willProvide default values only for undefined parameters
. Other “falsy” values (such as “, “”, false, NULL, 0, and NaN) will not be replaced with default values.
function createMicrobrewery(name) {
const breweryName = name || "Hipster Brew Co.";
// ...
}
Copy the code
function createMicrobrewery(name = "Hipster Brew Co.") {
// ...
}
Copy the code
2. Fewer parameters are required3
A ✅
It is necessary to limit the number of function arguments to make it easier to test functions. When multiple arguments are really needed, consider encapsulating them into an object.
function createMenu(title, body, buttonText, cancellable) {
// ...
}
createMenu("Foo"."Bar"."Baz".true);
Copy the code
var menuConfig = {
title: 'Foo'.body: 'Bar'.buttonText: 'Baz'.cancellable: true
}
function createMenu(menuConfig) {... }Copy the code
3. Single responsibility ✅
Single-purpose functions are easy to refactor, test, and understand
function emailClients(clients) {
clients.forEach(client= > {
let clientRecord = database.lookup(client);
if(clientRecord.isActive()) { email(client); }}); }Copy the code
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();
}
Copy the code
4. Clearly state the function ✅
function addToDate(date, month) {
// ...
}
const date = new Date(a);// It's hard to tell from the function name what is added
addToDate(date, 1);
Copy the code
function addMonthToDate(month, date) {
// ...
}
const date = new Date(a); addMonthToDate(1, date);
Copy the code
5. Object.assign overrides the default value ✅
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);
Copy the code
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}
Copy the code
6. Avoid duplicate code ❌
Repetitive code means that logic changes in more than one place.
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);
});
}
Copy the code
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);
});
}
Copy the code
7. Avoid using flag as the parameter ❌
The use of flag values means that this function does more than one thing, violating the single rule. If flag isboolean
Value, should consider repartitioning the function.
function createFile(name, temp) {
if (temp) {
fs.create(`./temp/${name}`);
} else{ fs.create(name); }}Copy the code
function createFile(name) {
fs.create(name);
}
function createTempFile(name) {
createFile(`./temp/${name}`);
}
Copy the code
8. Avoid function side effects ❌
A side effect is when a function does something other than “accept a value and return a result” (such as modifying an external global variable) that causes side effects.
const addItemToCart = (cart, item) = > {
cart.push({ item, date: Date.now() });
};
Copy the code
const addItemToCart = (cart, item) = > {
return [...cart, { item, date: Date.now() }];
};
Copy the code
8. Avoid global functions ❌
It’s bad practice to pollute global, because you might run into conflict with another library and have unexpected results during development.
For example, if you want to extend Array in JS and add a diff function to show the difference between two arrays, how do you do that? You could write diff to array. prototype, but doing so would conflict with other libraries with similar requirements. What if another library’s requirement for diff is to compare the differences between the first and last elements in an array?
Simple extensions to global arrays using classes in ES6 are obviously a better choice.
Array.prototype.diff = function(comparisonArray) {
const hash = new Set(comparisonArray);
return this.filter(item= >! hash.has(item)) }Copy the code
class SuperArray extends Array {
constructor(. args) {
super(... args); }diff(comparisonArray) {
const hash = new Set(comparisonArray);
return this.filter(item= >! hash.has(item)) } }Copy the code
9. Functional programming ❌
Functional programmingAccepts a value and returns a result
To make functions cleaner and easier to test
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;
}
Copy the code
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
);
Copy the code
Objects and Data Structures
1. Getter & Setter ✅
Using getters and setters to access data on an object may be better than simply looking up properties on the object:
- Get uniformly processes the returned results, making adjustment more convenient.
- When set values, you can easily add validation.
- Internal encapsulation function realization.
- When get or set, it is easy to catch logs and errors.
- Asynchronous acquisition can be achieved.
function makeBankAccount() {
// ...
return {
balance: 0
// ...
};
}
const account = makeBankAccount();
account.balance = 100;
Copy the code
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);
Copy the code
2. Privatized target members ✅
This can be done through closures
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
Copy the code
function makeEmployee(name) {
return {
getName() {
returnname; }}; }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
Copy the code
4. Classes
1. Use ES6 classes instead of the ES5 constructor ✅
ES6’s classes make writing object prototypes much cleaner and more like the syntax of object-oriented programming
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() {};
Copy the code
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() {
/ *... * /}}Copy the code
2. Chain method ✅
The chained approach makes code less redundant and more expressive. There are arguments that the method chain is not clean enough and violatedDemeter’s rule. However, this approach is very useful in JS and many libraries (such as JQuery and Moment).
Returning this in the class function makes it easy to chain methods that the class needs to execute. Counterexample \color{red}{counterexample} counterexample
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();
Copy the code
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();
Copy the code
3. Composition is better than inheritance ✅
Scenarios where inheritance is better than composition:
- Your inheritance represents an IS-A relationship, not a HAS-A relationship (Human->Animal vs User->UserDetails).
- You can reuse base class code (people can move like all animals).
- Global changes are made to the derived classes by changing the base class (changing the caloric expenditure of all animals in motion).
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(a);this.ssn = ssn;
this.salary = salary;
}
// ...
}
Copy the code
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);
}
// ...
}
Copy the code
5. Object-oriented Design (SOLID)
Single Responsibility Principle (SRP)
As mentioned in the above function, a class has too many miscellaneous functions, it needs to be broken up, a class does only one thing counterexample \color{red}{counterexample} counterexample
class UserSettings {
constructor(user) {
this.user = user;
}
changeSettings(settings) {
if (this.verifyCredentials()) {
// ...}}verifyCredentials() {
// ...}}Copy the code
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()) {
// ...}}}Copy the code
2, Open/Closed Principle (OCP)
It should be open for external extension, but closed for internal modification. In plain English, it only allows users to add new functionality without changing existing code. Counterexample \color{red}{counterexample} counterexample
class AjaxAdapter extends Adapter {
constructor() {
super(a);this.name = "ajaxAdapter"; }}class NodeAdapter extends Adapter {
constructor() {
super(a);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
}
Copy the code
class AjaxAdapter extends Adapter {
constructor() {
super(a);this.name = "ajaxAdapter";
}
request(url) {
// request and return promise}}class NodeAdapter extends Adapter {
constructor() {
super(a);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}); }}Copy the code
3, Liskov Substitution Principle (LSP)
Objects in a derived class (subclass) can replace objects in its base class (superclass) in a program.
If you have a parent class and a subclass, then the same call to the parent class and subclass results in the same result. This can be confusing, so let’s take a look at the classic square-rectangle example. Mathematically, a square is a rectangle, but if you model it by inheritance using the IS-A relationship, you will find that this is wrong.
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);
Copy the code
class Shape {
setColor(color) {
// ...
}
render(area) {
// ...}}class Rectangle extends Shape {
constructor(width, height) {
super(a);this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height; }}class Square extends Shape {
constructor(length) {
super(a);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);
Copy the code
4. Interface Segregation Principle (ISP)
JavaScript has no interfaces, so this principle is not as strict as others. However, this is important even if JavaScript lacks a type system.
“Customers should not be forced to rely on interfaces they do not use,” the ISP states. Interfaces are implicit contracts in JavaScript, thanks to Duck typing.
A good example of demonstrating this principle in JavaScript is a class that requires a large setup object. It’s helpful not to require clients to set a lot of options, because most of the time they don’t need all of them. Making them optional helps avoid “fat interfaces.” Counterexample \color{red}{counterexample} counterexample
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.
// ...
});
Copy the code
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(){}}});Copy the code
3, Dependency Inversion Principle (DIP)
This principle has two core points:
- High-level modules should not depend on low-level modules. They should all rely on abstract interfaces.
- Abstract interfaces should be separated from concrete implementations, which should depend on abstract interfaces.
AngularJS implements this principle in the form of Dependency Injection (DI). Although they are not the same concept, DIP prevents a high-level module from knowing its low-level module implementation and setting them up. It can do this through DI. A huge benefit of doing this is that it reduces coupling between modules. Coupling is a very bad development pattern because it makes code difficult to refactor.
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();
Copy the code
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();
Copy the code