Source
I think the article is so good to read and rewrite, mainly for me and you to write code with principles, easy to read, easy to understand, easy to maintain and reuse code , I have a reference at:
https://github.com/hienvd/clean-code-javascript/
https://github.com/ryanmcdermott/clean-code-javascript
Introduce
Software engineering principles, from Robert C. Martin’s book Clean Code , are applied to the JavaScript language. This is not a tutorial on how to write Javascript code, it is a guide on how to write code that is easy to understand, reuse and refactor in Javascript.
Not all of the principles here must be strictly followed, and even a few of them are in common use. Here, it’s just a guide – nothing more, but it is codified through the experience gained over the years of the author of the book Clean Code.
Software engineering has been in development for more than 50 years, and we are still learning a lot. Once software architecture becomes popular, perhaps then we will have more difficult rules to follow. For now, let these tutorials serve as a standard to evaluate the quality of the JavaScript code that you and your team generate.
Just knowing these guidelines will not immediately make you a better software developer, and working with them for many years does not mean that you will not make any mistakes. Each piece of code begins as a first draft, like clay molded and it will eventually reveal its shape. Finally, we trim the defect when we revisit it with our colleagues. Do not let yourself be defeated by the first draft, which still needs to be edited. Defeat the lines of code instead.
Apply
Out
Using variable names means:
- Not good :
1 2 3 | // yyyymmdd là cái gì ?? const yyyymmdd = moment().format('YYYY/MM/DD'); |
- Good:
1 2 3 | // ít nhất thì mình vẫn hiểu đây là ngày hiện tại const currentDate = moment().format('YYYY/MM/DD'); |
Use the same vocabulary for the same type of variable:
- Not good :
1 2 3 4 | getUserInfo(); getClientData(); getCustomerRecord(); |
- Good:
1 2 | getUser(); |
Use easy-to-understand and searchable names:
We will read more code than write them. The important thing is that the code we write is readable and searchable. The naming of variables has no semantics compared to the program, we will probably hurt the reader of the code. Make your variable names searchable. Tools like buddy.js and ESLint can help identify unnamed constants .
- Not good :
1 2 3 | // 86400000 là cái quái gì thế? setTimeout(blastOff, 86400000); |
- Good:
1 2 3 4 5 | // MILLISECONDS_IN_A_DAY chúng ta hiểu phải không các bạn const MILLISECONDS_IN_A_DAY = 86400000; setTimeout(blastOff, MILLISECONDS_IN_A_DAY); |
In my opinion, there should be a folder to define these shared constants .
Use explanatory variables:
- Not good :
1 2 3 4 5 6 | const address = 'One Infinite Loop, Cupertino 95014'; const cityZipCodeRegex = /^[^,\]+[,\s]+(.+?)s*(d{5})?$/; // mình đọc cái đống này thật ra cũng không hiểu saveCityZipCode(address.match(cityZipCodeRegex)[1], address.match(cityZipCodeRegex)[2]); |
- Good:
1 2 3 4 5 6 7 | const address = 'One Infinite Loop, Cupertino 95014'; const cityZipCodeRegex = /^[^,\]+[,\s]+(.+?)s*(d{5})?$/; const [, city, zipCode] = address.match(cityZipCodeRegex) || []; // đến đây thì mình hiểu đó là city và zipCode truyền vào saveCityZipCode(city, zipCode); |
Avoid harming the brain of others
Until now, not only in programming but everything in life, if clear, of course it would still be easier to understand.
- Not good :
1 2 3 4 5 6 7 | const locations = ['Austin', 'New York', 'San Francisco']; locations.forEach((l) => { //... // Khoan, cái 'l' gì thế ? dispatch(l); }); |
- Good:
1 2 3 4 5 6 7 | const locations = ['Austin', 'New York', 'San Francisco']; locations.forEach((location) => { // ... // Hiểu cái `l` lúc nãy là cái gì rồi phải không các bạn :P dispatch(location); }); |
Do not add unnecessary contexts
If the name of the class , the name of the Object says something, at least what it is, there is no need to repeat it in the variable name.
- Not good :
1 2 3 4 5 6 7 8 9 10 | const Car = { carMake: 'Honda', carModel: 'Accord', carColor: 'Blue' }; function paintCar(car) { car.carColor = 'Red'; } |
- Good:
1 2 3 4 5 6 7 8 9 10 | const Car = { make: 'Honda', model: 'Accord', color: 'Blue' }; function paintCar(car) { car.color = 'Red'; } |
It is better to use the default parameters than to check
- Not good :
1 2 3 4 5 | function createMicrobrewery(name) { const breweryName = name || 'Hipster Brew Co.'; // ... } |
- Good:
1 2 3 4 | function createMicrobrewery(breweryName = 'Hipster Brew Co.') { // ... } |
Jaw
Function argument (ideally less than or equal to 2)
Number param (parameters) passed in 1 function is extremely important: It makes you at least look better, easier test than, In case of more than 3 params can cause you to test a ton of tests Different cases with separate arguments.
1 or 2 param is ideal for a function , but there are also cases of force majeure, it is best to avoid passing more than 2 params , cases from 3 params , we should combine.
Usually, transferring as many param to the function, the function will increasingly handle more work.
Since Javascript allows creating multiple Objects quickly, without the need for multiple classes , you can use an Object if you need to pass multiple parameters .
To make it clear what a function is expecting, you can use the destructuring structure of ES6 . This has several advantages:
- When someone looks at the function , which properties are used will become immediately obvious.
- Destructuring also copies the specified initial values of the Object param passed into the function . This can help prevent side effects. Note: Objects and arrays that are destructure from Object param cannot be copied.
- Linter will probably warn you about unused properties, which would not have happened without destructuring .
- Not good :
1 2 3 4 | function createMenu(title, body, buttonText, cancellable) { // ... } |
- Good:
1 2 3 4 5 6 7 8 9 10 11 | function createMenu({ title, body, buttonText, cancellable }) { // ... } createMenu({ title: 'Foo', body: 'Bar', buttonText: 'Baz', cancellable: true }); |
The function should only solve one problem
This is the most important rule of software engineering. When a function does more than one thing, it becomes more difficult to code, test, and deduce.
When you can isolate a function to perform only one action, it will be easier to refactor and your code will be much more readable. If you only need to follow this guide and do nothing else, you are better than many other developers .
- Not good :
1 2 3 4 5 6 7 8 9 | function emailClients(clients) { clients.forEach((client) => { const clientRecord = database.lookup(client); if (clientRecord.isActive()) { email(client); } }); } |
- Good:
1 2 3 4 5 6 7 8 9 10 11 | function emailClients(clients) { clients .filter(isClientActive) .forEach(email); } function isClientActive(client) { const clientRecord = database.lookup(client); return clientRecord.isActive(); } |
Function names must say what they do
Just by looking at the function name, you or others understand what the function is for.
- Not good :
1 2 3 4 5 6 7 8 9 | function addToDate(date, month) { // ... } const date = new Date(); // Khó để biết được hàm này thêm gì thông qua tên hàm. addToDate(date, 1); |
- Good:
1 2 3 4 5 6 7 | function addMonthToDate(month, date) { // ... } const date = new Date(); addMonthToDate(1, date); |
The function should only have an abstract class
When there is more than one abstract class, your function is doing too much. Breaking down functions will make testing and reuse easier.
- Not good :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | function tokenize(code) { const REGEXES = [ // ... ]; const statements = code.split(' '); const tokens = []; REGEXES.forEach((REGEX) => { statements.forEach((statement) => { tokens.push( /* ... */ ); }); }); return tokens; } function lexer(tokens) { const ast = []; tokens.forEach((token) => { ast.push( /* ... */ ); }); return ast; } function parseBetterJSAlternative(code) { const tokens = tokenize(code); const ast = lexer(tokens); ast.forEach((node) => { // parse... }); } |
Avoid code duplication
Having two functions that handle the same function is a must to avoid, that is, duplicate code . Duplicate code is not good because if you need to change the same logic, you have to fix it in more than one place.
Imagine if you ran a restaurant and you track inventory: including tomatoes, onions, garlic, spices, etc. If you have multiple management lists, all of them must be changed when you serve a dish containing tomatoes. If you only have 1 listing, the update is in one place.
Usually, you have repeated lines of code because you have 2 or more things that differ only slightly, but share many things in common, but their differences require you to have 2 or more separate functions to do many similar things. Erase the identical lines of code means creating an abstraction that can handle file differences with just one function / module or class.
Having the right abstraction is very important, which is why you should adhere to the SOLID principles set out in the class section. Bad abstraction can be worse than duplicated code , so be careful! If you can create a good abstraction , do it! Do not repeat yourself, if you do not want to go to many places updated whenever you want to change something.
- Not good :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | function showEmployeeList(employees) { employees.forEach((employee) => { const expectedSalary = employee.calculateExpectedSalary(); const experience = employee.getExperience(); let portfolio = employee.getGithubLink(); if (employee.type === 'manager') { portfolio = employee.getMBAProjects(); } const data = { expectedSalary, experience, portfolio }; render(data); }); } |
Set default objects with Object.assign
- Not good :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | const menuConfig = { title: 'Order', // User did not include 'body' key buttonText: 'Send', cancellable: true }; function createMenu(config) { config = Object.assign({ title: 'Foo', body: 'Bar', buttonText: 'Baz', cancellable: true }, config); // config now equals: {title: "Order", body: "Bar", buttonText: "Send", cancellable: true} // ... } createMenu(menuConfig); |
Do not use flags as function arguments
Flag variables tell your users that the function does more than one thing. The function should only do one task. So separate your functions if they are making branching code based on a boolean variable .
- Not good :
1 2 3 4 5 6 7 8 | function createFile(name, temp) { if (temp) { fs.create(`./temp/${name}`); } else { fs.create(name); } } |
- Good:
1 2 3 4 5 6 7 8 | function createFile(name) { fs.create(name); } function createTempFile(name) { createFile(`./temp/${name}`); } |
Do not use flags as function arguments
A function creates side effects if it does anything other than receiving an input value and returning one or more values. Side effects can be recording a file , changing some global variables , or accidentally giving all your money to a stranger.
Now, sometimes you need side effects in a program. As in the previous example, you need to write a file . What you need to do is focus on where you are going to do it. Do not write separate functions and classes to create a specific file . Have a service to write it. One and only.
The main point is to avoid common errors such as sharing state between objects without any structure, using mutable data types that can be written by anything, and not focus where side effects may occur. If you can do that, you’ll be happier than most other developers .
- Not good :
1 2 3 4 5 6 7 8 9 10 11 12 | // Biến toàn cục được tham chiếu bởi hàm dưới đây. // Nếu chúng ta có một hàm khác sử dụng name, nó sẽ trở thành một array let name = 'Ryan McDermott'; function splitIntoFirstAndLastName() { name = name.split(' '); } splitIntoFirstAndLastName(); console.log(name); // ['Ryan', 'McDermott']; |
- Good:
1 2 3 4 5 6 7 8 9 10 | function splitIntoFirstAndLastName(name) { return name.split(' '); } const name = 'Ryan McDermott'; const newName = splitIntoFirstAndLastName(name); console.log(name); // 'Ryan McDermott'; console.log(newName); // ['Ryan', 'McDermott']; |
Avoiding side effects (part 2)
In JavaScript , basic types are passed by value and objects / arrays are passed by reference . In the case of objects and arrays , for example if our function makes a change in a shopping cart array , such as adding a product to purchase, any other function that uses the ‘shopping cart’ array will affected by this addition. This may be fine, but it can also get worse. Imagine the following bad case:
The user clicks “Buy”, the purchase button will call the purchase function, which generates a network request and sends the basket array to the server . Due to a slow connection, the purchase function may keep retrying the request. Now, what if users accidentally clicked the “Add to Cart” button on a product they didn’t really want before the network made a request? If that happens and the network starts sending a request, the buy function will inadvertently add a product because it has a basket array reference that the function that adds the product to the shopping cart has changed by adding a product that they do not want.
A good solution is to add the product to the shopping cart to always make a copy of the basket, change it, and return that copy. This ensures that no function that holds the shopping cart reference is affected by any changes.
Two notes for this approach:
- There may be situations where you really want to change the input object, but when you apply this method you will find that these cases are rare. Most problems can be restructured so they no longer have side effects.
- Cloning large objects can affect performance. Fortunately, that’s not a big deal in practice because having immutable-js allows this approach to be faster and use less memory than when you manually copy objects and arrays .
- Not good :
1 2 3 4 | const addItemToCart = (cart, item) => { cart.push({ item, date: Date.now() }); }; |
- Good:
1 2 3 4 | const addItemToCart = (cart, item) => { return [...cart, { item, date : Date.now() }]; }; |
Do not write to global functions
Influencing global variables is a bad practice in JavaScript because you may conflict with other libraries and your API users will not know it until an error occurs on the product.
Think of this example: what if you wanted to extend the Array method in native JavaScript so you could have a diff
function that shows the difference between the two arrays ? You could write a new function with Array.prototype
, but it might conflict with another library that did the same thing. What if the library only used diff
to find the difference between the first and last element of an array ? That’s why it would be much better to just use the ES2015 / ES6 classes and simply extend the Array globally.
- Not good :
1 2 3 4 5 | Array.prototype.diff = function diff(comparisonArray) { const hash = new Set(comparisonArray); return this.filter(elem => !hash.has(elem)); }; |
- Good:
1 2 3 4 5 6 7 | class SuperArray extends Array { diff(comparisonArray) { const hash = new Set(comparisonArray); return this.filter(elem => !hash.has(elem)); } } |
Support function rather than imperative programming
JavaScript is not a functional programming language like Haskell , but it has its function . Functional languages are cleaner and easier to test. Use this programming when you can.
- Not good :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | const programmerOutput = [ { name: 'Uncle Bobby', linesOfCode: 500 }, { name: 'Suzie Q', linesOfCode: 1500 }, { name: 'Jimmy Gosling', linesOfCode: 150 }, { name: 'Gracie Hopper', linesOfCode: 1000 } ]; const INITIAL_VALUE = 0; const totalOutput = programmerOutput .map((programmer) => programmer.linesOfCode) .reduce((acc, linesOfCode) => acc + linesOfCode, INITIAL_VALUE); |
Packaging conditions
- Not good :
1 2 3 4 | if (fsm.state === 'fetching' && isEmpty(listNode)) { // ... } |
- Good:
1 2 3 4 5 6 7 8 | function shouldShowSpinner(fsm, listNode) { return fsm.state === 'fetching' && isEmpty(listNode); } if (shouldShowSpinner(fsmInstance, listNodeInstance)) { // ... } |
Avoid negative conditions
- Not good :
1 2 3 4 5 6 7 8 | function isDOMNodeNotPresent(node) { // ... } if (!isDOMNodeNotPresent(node)) { // ... } |
- Good:
1 2 3 4 5 6 7 8 | function isDOMNodePresent(node) { // ... } if (isDOMNodePresent(node)) { // ... } |
Avoid the conditions
This seems like an impossible task. On hearing this first, most people say, “How do I need to do without the if clause?” The answer is that you can use polymorphism to achieve the same job in a lot of cases.
The second question is usually “That’s good but why do I want to do it?” The answer is the concept we learned earlier: a function should only do one thing. When you have multiple classes and functions that have multiple if
clauses, you are telling your users that your function is doing more than one thing. Remember, only one job.
- Not good :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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(); } } |
Avoid type checking (part 1)
JavaScript is not typed, meaning your function can accept any type argument . Sometimes you are tempted by this freedom and easily lead to type checking in your jaw . There are many ways to avoid having to do this. The first thing is to consider using consistent APIs .
- Not good :
1 2 3 4 5 6 7 8 | function travelToTexas(vehicle) { if (vehicle instanceof Bicycle) { vehicle.peddle(this.currentLocation, new Location('texas')); } else if (vehicle instanceof Car) { vehicle.drive(this.currentLocation, new Location('texas')); } } |
- Good:
1 2 3 4 | function travelToTexas(vehicle) { vehicle.move(this.currentLocation, new Location('texas')); } |
Avoid type checking (part 2)
If you work with basic types like strings , integers, and arrays , and you can’t use polymorphism but you still feel you need to type check, you should consider using TypeScript . It is a great alternative to regular JavaScript , because it provides a static type in addition to the standard JavaScript syntax. The problem with manual type checking is that doing this well requires a lot of lengthy time and this fake “security type” is no substitute for losing the readability of the code. Keep your JavaScript code clean, write good tests and have good code reviews. If not then do all that but with TypeScript (like I said, it’s a good replacement!).
- Not good :
1 2 3 4 5 6 7 8 9 | 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:
1 2 3 4 | function combine(val1, val2) { return val1 + val2; } |
Do not be too optimal
Modern browsers do a lot of optimization below at run time. A lot of times, if you are optimizing then you are wasting your own time. See here to know when optimization is lacking. Make those optimizations and until they are fixed if possible.
- Not good :
1 2 3 4 5 6 | // Trên các trình duyệt cũ, mỗi lần lặp với 'list.length` chưa được cache sẽ tốn kém // vì `list.length` sẽ được tính lại. Trong các trình duyệt hiện đại, điều này đã được tối ưu. for (let i = 0, len = list.length; i < len; i++) { // ... } |
- Good:
1 2 3 4 | for (let i = 0; i < list.length; i++) { // ... } |
Remove dead code
Dead code is as bad as duplicate code . There is no reason to keep them in your codebase . If it is not called, discard it! It will still be in your version history if you still need it.
- Not good :
1 2 3 4 5 6 7 8 9 10 11 | function oldRequestModule(url) { // ... } function newRequestModule(url) { // ... } const req = newRequestModule; inventoryTracker('apples', req, 'www.inventory-awesome.io'); |
- Good:
1 2 3 4 5 6 7 | function newRequestModule(url) { // ... } const req = newRequestModule; inventoryTracker('apples', req, 'www.inventory-awesome.io'); |
Objects and Data Structures
Use getter and setter
JavaScript has no interface
or type so it is difficult to implement this model, because we do not have keywords like public
and private
. So using getters
and setters
to access data on objects is better than simply looking for an attribute on an object . You may ask “Why?”.
Here is a list of reasons why:
- When you want to do more than getting an object property, you don’t need to search and change every
accessor
in your codebase . - Makes adding simple validation when done on a set.
- Packing of internal representations.
- Easily add logs and handle errors when
getting
andsetting
. - Inheriting this class , you can
override
the default functions . - You can lazy load the properties of an object , get it from the server .
- Not good :
1 2 3 4 5 6 7 8 9 10 11 12 | function makeBankAccount() { // ... return { balance: 0, // ... }; } const account = makeBankAccount(); account.balance = 100; |
- Good:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | function makeBankAccount() { // this one is private let balance = 0; // Một "getter", thiết lập public thông qua đối tượng được trả về dưới đây function getBalance() { return balance; } // Một "setter", thiết lập public thông qua đối tượng được trả về dưới đây function setBalance(amount) { // ... validate before updating the balance balance = amount; } return { // ... getBalance, setBalance, }; } const account = makeBankAccount(); account.setBalance(100); |
Make the object private members
This can be done via closures (for ES5 and older).
- Not good :
1 2 3 4 5 6 7 8 9 10 11 12 | function makeBankAccount() { // ... return { balance: 0, // ... }; } const account = makeBankAccount(); account.balance = 100; |
- Good:
1 2 3 4 5 6 7 8 9 10 11 | const Employee = function (name) { this.getName = function getName() { return 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: John Doe |
Class
Prioritize the ES2015 / ES6 class over the pure ES5 functions
It is difficult to read the inheritance class , constructor class , and method definitions in classic ES5 classes . If you need inheritance (and note that you may not), it is better to use the class . However priority uses smaller function layer until you need large objects and complex.
- Not good :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | 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:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | 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() { /* ... */ } } |
Use functions in succession
This is a very useful pattern in JavaScript and you see it in many libraries such as jQuery and Lodash .
It allows your code to be streamlined and concise. For that reason, in my opinion, use the method of sequential functions and let’s see how clean your code is. In class functions , simply return this
at the end of each function, and you can chain other methods into it.
- Not good :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | class Car { constructor() { this.make = 'Honda'; this.model = 'Accord'; this.color = 'white'; } 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(); car.setColor('pink'); car.setMake('Ford'); car.setModel('F-150'); car.save(); |
- Good:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | class Car { constructor() { this.make = 'Honda'; this.model = 'Accord'; this.color = 'white'; } setMake(make) { this.make = make; // Ghi chú: Trả về this để xâu chuỗi các phương thức return this; } setModel(model) { this.model = model; // Ghi chú: Trả về this để xâu chuỗi các phương thức return this; } setColor(color) { this.color = color; // Ghi chú: Trả về this để xâu chuỗi các phương thức return this; } save() { console.log(this.make, this.model, this.color); // Ghi chú: Trả về this để xâu chuỗi các phương thức return this; } } const car = new Car() .setColor('pink') .setMake('Ford') .setModel('F-150') .save(); |
Component precedence over inheritance
As emphasized in the Design Patterns of Gang of Four , you should use component structure rather than inheritance if possible. There are many good reasons to use inheritance as well as use components. The highlight of this motto is that if your mind follows inheritance instinct, think about if the component can better model your problem. In some cases it is possible.
You might be wondering, “When should I use inheritance?” It depends on the issue at your fingertips, but here is a decent list of when inheritance makes more sense than ingredients:
- Your inheritance represents each “is-a” relationship and not each “has-a” relationship (Human-> Animal vs. User-> UserDetails).
- You can reuse code from the base class (Humans can move like all Animals).
- You want to make global changes to derived classes by changing the base class. (Change the calories of all animals as they move)
- Not good :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | class Employee { constructor(name, email) { this.name = name; this.email = email; } // ... } // Không tốt bởi vì Employees "có" dữ liệu thuế. // EmployeeTaxData không phải là một loại của Employee class EmployeeTaxData extends Employee { constructor(ssn, salary) { super(); this.ssn = ssn; this.salary = salary; } // ... } |
- Good:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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
Single Responsibility Principle
As stated in the Clean Code , “Only one class can be changed for a single reason”. It is fascinating to cram a lot of functions into one layer , just like when you can only get one suitcase for a flight. The problem is that your class will not be conceptually cohesive and there will be plenty of reasons to change. It is important to minimize the number of times you need to change a class . It is important because if there are too many functions in a class and you just want to change a bit of that class , then it may be difficult to understand that changes will affect other modules in the codebase of. how are you.
- Not good :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | class UserSettings { constructor(user) { this.user = user; } changeSettings(settings) { if (this.verifyCredentials()) { // ... } } verifyCredentials() { // ... } } |
- Good:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | 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()) { // ... } } } |
Open / Closed Principle
Betrand Meyer said “can comfortably expand a module, but limited amendments within that module.” What does it mean? This principle basically emphasizes that you must allow users to add new functions without changing existing code .
- Not good :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | 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 === 'httpNodeAdapter') { return makeHttpCall(url).then((response) => { // transform response and return }); } } } function makeAjaxCall(url) { // request and return promise } function makeHttpCall(url) { // request and return promise } |
- Good:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | 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 Substitution Principle
This is a scary term for a very simple concept. It is formally defined as: “If S is a subtype of T, then objects of type T can be replaced with objects of type S (for example, objects of type S may be replace objects of type T) without changing any of the desired properties of the program (accuracy, task execution, etc.), which is an even more frightening definition. .
The best explanation for this principle is that, if you have a parent class and a subclass, then the base and subclasses can be used interchangeably without altering the correctness of the program. It may still be a little confusing here, so let’s look at the classic Square-Rectangle example below. Mathematically, a square is a rectangle, but if you model this using the “is a” relation through inheritance, you’ll quickly get into trouble.
- Not good :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | 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: Will return 25 for Square. Should be 20. rectangle.render(area); }); } const rectangles = [new Rectangle(), new Rectangle(), new Square()]; renderLargeRectangles(rectangles); |
- Good:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | 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); |
Interface Segregation Principle
JavaScript has no interface so this principle does not apply as strictly as other principles. However, it is also important and relevant even with the JavaScript- deficient system.
The principle of interface separation emphasizes that “Users should not be forced to depend on the interfaces they do not use.” Interfaces are hidden constraints in JavaScript because of duck typing .
A good example to illustrate this principle in JavaScript are classes that require the installation of large objects. Not asking the user to set a large number of options is a benefit, because most of the time they don’t need all the settings. Making them optional helps to avoid having a “fat interface”.
- Not good :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | class DOMTraverser { constructor(settings) { this.settings = settings; this.setup(); } setup() { this.rootNode = this.settings.rootNode; this.animationModule.setup(); } traverse() { // ... } } const $ = new DOMTraverser({ rootNode: document.getElementsByTagName('body'), animationModule() {} // Most of the time, we won't need to animate when traversing. // ... }); |
- Good:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | 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() {} } }); |
Dependency Inversion Principle
This principle confirms the following two essentials:
- But high-level modules should not depend on low-level modules . Both should depend on abstraction .
- Abstraction (interface) should not depend on details, but vice versa.
This may be confusing at first, but if you’ve worked with Angular.js, you’ve seen a realization of this principle in the form of Dependency Injection (DI). When they are not the same concepts, DIP keeps its high level modules unaware of its low level modules and sets them up. This can be achieved through DI. A great benefit of DIP is that it reduces the interdependence between modules. Interdependence is a bad pattern, because it makes refactoring difficult.
As asserted earlier, JavaScript has no interface so abstractions that depend on are hidden constraints. That is to say, the methods and properties that one object / class expose to another object / class. In the example below, the implicit constraint is that any Request module for an InventoryRequester will have a requestItems method.
- Not good :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | class InventoryRequester { constructor() { this.REQ_METHODS = ['HTTP']; } requestItem(item) { // ... } } class InventoryTracker { constructor(items) { this.items = items; // Không tốt: chúng ta đã tạo một phụ thuộc vào một hiện thực của một request cụ thể // Chúng ta nên có những requestItems phụ thuộc vào một phương thức request `request` this.requester = new InventoryRequester(); } requestItems() { this.items.forEach((item) => { this.requester.requestItem(item); }); } } const inventoryTracker = new InventoryTracker(['apples', 'bananas']); inventoryTracker.requestItems(); |
- Good:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | 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) { // ... } } // Bằng cách xây dựng các phụ thuộc ở ngoài và thêm chúng vào, chúng ta có thể // dễ dàng thay thế module request bằng một module mới lạ sử dụng WebSockets. const inventoryTracker = new InventoryTracker(['apples', 'bananas'], new InventoryRequesterV2()); inventoryTracker.requestItems(); |
Testing
Testing is more important than shipping. If you don’t have the test or it’s not enough, every time you ship your code you’re not sure if you will damage anything. It is up to your team to decide what constitutes a sufficient number of tests, but having 100% coverage (all statements and branches) is a way for you to gain high confidence. This means that in addition to having a good framework for testing, you also need to use a good coverage tool .
There is no reason not to write tests. There are many good JS test frameworks , so look for one you like. Once you’ve found the right one for your team, set a goal to always write tests for each of your new features or modules. If your favorite test method is Test Driven Development (TDD), that’s great, but it’s important to make sure you reach your coverage goal before launching a feature or refactor an old feature. somehow.
A unique concept for each unit of test
- Not good :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | const assert = require('assert'); describe('MakeMomentJSGreatAgain', () => { it('handles date boundaries', () => { let date; date = new MakeMomentJSGreatAgain('1/1/2015'); date.addDays(30); date.shouldEqual('1/31/2015'); date = new MakeMomentJSGreatAgain('2/1/2016'); date.addDays(28); assert.equal('02/29/2016', date); date = new MakeMomentJSGreatAgain('2/1/2015'); date.addDays(28); assert.equal('03/01/2015', date); }); }); |
- Good:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | const assert = require('assert'); describe('MakeMomentJSGreatAgain', () => { it('handles 30-day months', () => { const date = new MakeMomentJSGreatAgain('1/1/2015'); date.addDays(30); date.shouldEqual('1/31/2015'); }); it('handles leap year', () => { const date = new MakeMomentJSGreatAgain('2/1/2016'); date.addDays(28); assert.equal('02/29/2016', date); }); it('handles non-leap year', () => { const date = new MakeMomentJSGreatAgain('2/1/2015'); date.addDays(28); assert.equal('03/01/2015', date); }); }); |
Concurrent processing
Use Promise, not callbacks
Callbacks are not so ‘clean’, they cause too much nested code (callback hell). Since ES2015 / ES6, Promise has been included in Javascript. Use them!
- Not good :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | require('request').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', (requestErr, response) => { if (requestErr) { console.error(requestErr); } else { require('fs').writeFile('article.html', response.body, (writeErr) => { if (writeErr) { console.error(writeErr); } else { console.log('File written'); } }); } }); |
- Good:
1 2 3 4 5 6 7 8 9 10 11 | require('request-promise').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin') .then((response) => { return require('fs-promise').writeFile('article.html', response); }) .then(() => { console.log('File written'); }) .catch((err) => { console.error(err); }); |
Async / Await is ‘cleaner’ than Promise
Promise is a ‘clean’ alternative to callbacks, but ES2017 / ES8 introduces async and await, which is an even better solution than Promise. What you need to do is a function with the async keyword prefix, and you can write logical commands without a key sequence of functions. Use this if you can take advantage of ES2017 / ES8 features today!
- Not good :
1 2 3 4 5 6 7 8 9 10 11 | require('request-promise').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin') .then((response) => { return require('fs-promise').writeFile('article.html', response); }) .then(() => { console.log('File written'); }) .catch((err) => { console.error(err); }); |
- Good:
1 2 3 4 5 6 7 8 9 10 | async function getCleanCodeArticle() { try { const response = await require('request-promise').get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin'); await require('fs-promise').writeFile('article.html', response); console.log('File written'); } catch(err) { console.error(err); } } |
Error handling
Do not ignore the errors that have been caught
If you do nothing about the error, you will not be able to fix or respond to the error. It is also not much better to write errors to the console
(console.log) because most of it can be lost in a bunch of things displayed in the console. If you put any code in a try / catch block, you think an error might occur here, so you should have a solution or create a code stream to handle the error when it happens.
- Not good :
1 2 3 4 5 6 | try { functionThatMightThrow(); } catch (error) { console.log(error); } |
- Good:
1 2 3 4 5 6 7 8 9 10 11 12 | 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! } |
Do not ignore rejected promises.
Same cause as above.
- Not good :
1 2 3 4 5 6 7 8 | getdata() .then((data) => { functionThatMightThrow(data); }) .catch((error) => { console.log(error); }); |
- Good:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | getdata() .then((data) => { functionThatMightThrow(data); }) .catch((error) => { // One option (more noisy than console.log): console.error(error); // Another option: notifyUserOfError(error); // Another option: reportErrorToService(error); // OR do all three! }); |
Format
The format of the code is subjective. Like many of the rules presented in this document, there are no rigid and quick rules that you must follow. The main point of this section is NEVER PICTURE about how to format the code. There are dozens of tools to automate this. Use a certain tool! It’s a waste of time and money just to argue about code formatting issues.
For things that are not within the scope of automatic code formatting (indentation, tabs and spaces, single and double quotes, etc.) see some instructions here.
Use uniform capitalization
Javascript is an untyped language, so capitalization will tell a lot about your variables, functions, and so on. These rules are subjective, so your team can choose which rules they want. However it is important that no matter how you choose to write, use it consistently in your codebase.
- Not good :
1 2 3 4 5 6 7 8 9 10 11 12 | 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:
1 2 3 4 5 6 7 8 9 10 11 12 | 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 {} |
Calling and calling functions should be near each other
If a function calls another function, keep these functions vertically near the file. Ideally, keep the calling function above the called function. We tend to read code from the top down, just like reading a newspaper. So let’s make our code read that way too.
- Not good :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | 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(user); review.perfReview(); |
- Good:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | 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(); |
Write a caption
Only write notes for things with complex logic.
Comments are usually apologies, not requests. Những đoạn code tốt thì đa số tự nó đã là tài liệu rồi.
- Không tốt :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | function hashIt(data) { // Khai báo hash let hash = 0; // Lấy chiều dài của chuỗi const length = data.length; // Lặp qua mỗi kí tự for (let i = 0; i < length; i++) { // Lấy mã của kí tự const char = data.charCodeAt(i); // Gán giá trị cho hash hash = ((hash << 5) - hash) + char; // Chuyển thành định dạng số nguyên 32 bit hash &= hash; } } |
- Tốt :
1 2 3 4 5 6 7 8 9 10 11 12 13 | 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; // Chuyển thành định dạng số nguyên 32 bit hash &= hash; } } |
Đừng giữ lại những đoạn code bị chú thích trong codebase của bạn.
Những công cụ quản lí phiên bản sinh ra để làm nhiệm vụ của chúng. Hãy để code cũ của bạn nằm lại trong dĩ vãng đi.
- Không tốt :
1 2 3 4 5 | doStuff(); // doOtherStuff(); // doSomeMoreStuff(); // doSoMuchStuff(); |
- Tốt :
1 2 | doStuff(); |
Đừng viết các chú thích nhật ký.
Hãy nhớ, sử dụng công cụ quản lí phiên bản! Chúng ta không cần những đoạn code vô dụng, bị chú thích và đặc biệt là những chú thích dạng nhật ký… Sử dụng git log
để xem lịch sử được mà!
- Không tốt :
1 2 3 4 5 6 7 8 9 10 | /** * 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; } |
- Tốt :
1 2 3 4 | function combine(a, b) { return a + b; } |
Tránh những đánh dấu vị trí
Chúng thường xuyên làm nhiễu code. Hãy để những tên hàm, biến cùng với các định dạng thích hợp tự tạo thành cấu trúc trực quan cho code của bạn.
- Không tốt :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | //////////////////////////////////////////////////////////////////////////////// // Scope Model Instantiation //////////////////////////////////////////////////////////////////////////////// $scope.model = { menu: 'foo', nav: 'bar' }; //////////////////////////////////////////////////////////////////////////////// // Action setup //////////////////////////////////////////////////////////////////////////////// const actions = function() { // ... }; |
- Tốt :
1 2 3 4 5 6 7 8 9 | $scope.model = { menu: 'foo', nav: 'bar' }; const actions = function() { // ... }; |
End
Tài liệu này cũng có sẵn ở các ngôn ngữ sau:
- English : ryanmcdermott/clean-code-javascript
- Brazilian Portuguese : fesnt/clean-code-javascript
- Chinese :
- German : marcbruederlin/clean-code-javascript
- Korean : qkraudghgh/clean-code-javascript-ko
- Spanish : andersontr15/clean-code-javascript
- Russian :