In the early days of web sites, JavaScript was mostly used to implement small dynamic effects or form validation. Today’s Web applications use tens of thousands of lines of JavaScript code to do all sorts of complex processing. These changes require maintainable capabilities to be a high priority for developers. Like software engineers in the more traditional sense, JavaScript engineers are hired to add value to the company. The mission of the modern front end engineer is not only to ensure that the product goes live on time, but also to accumulate knowledge assets for the company over time.
Writing maintainable code is important because many developers spend a lot of time maintaining code written by others. In real development, it is very rare to start from the first line of code. It’s often about building your work on top of someone else’s code. Making your code easy to maintain makes it easier for other developers to do their jobs.
Note that the concept of maintainable code doesn’t just apply to JavaScript; many of these concepts apply to any programming language. Of course, some of the concepts may indeed be specific to JavaScript.
- What is maintainable code
Maintainable code has several characteristics. In general, to say that code is maintainable means that it has the following characteristics.
- Easy to understand. Anyone who looks at the code knows what it does and how it is implemented without having to ask the original developer.
- Common sense. Everything in the code seems natural, no matter how complex the operation.
- Easy to fit. Even if the data changes, you don’t have to rewrite it completely.
- Easy to expand. The code architecture has been carefully designed to support future extensions to the core functionality.
- Easy to debug. When a problem occurs, the code can give you explicit information that can be used to directly locate the problem.
Being able to write maintainable JavaScript code is an important professional skill. This skill is an important difference between the business enthusiast who cobbles together a website in a weekend and the professional developer who thinks through everything he or she does.
- Coding standards
The first step in writing maintainable code is to seriously consider the coding specification. Coding specifications are covered in most programming languages, and a simple Internet search reveals thousands of related articles. Professional organizations have codes for developers that are designed to make code more maintainable. Good open source projects are organized with strict coding specifications that make it easy for everyone in the community to understand the code.
Coding conventions are important for JavaScript because the language is so flexible. Unlike most object-oriented languages, JavaScript does not force developers to define everything as an object. In contrast, JavaScript supports any programming style, including traditional object-oriented and declarative programming, as well as functional programming. A quick look at a few open source JavaScript libraries reveals that there are many ways to create objects, define methods, and manage environments.
The following sections discuss some of the basic aspects of coding specifications. All of these topics are important, but of course everyone’s needs are different and the approach can be different.
2.1 readability
For code to be easy to maintain, it must first be easy to understand. Readability must take into account that the code is a text file. For this reason, indentation is an important foundation for readability. If everyone uses the same indentation, the entire project’s code will be easier to read. Indentation is usually defined using Spaces rather than tabs, because the latter will appear differently in different text editors. Generally speaking, the indent is based on 4 Spaces as a unit, of course, the specific number can be decided.
Another aspect of readability is code comments. In most programming languages, it is widely accepted to write annotations for each method. Because JavaScript can create functions anywhere in your code, this is easy to overlook. Because of this, it’s probably more important to comment every function in JavaScript. In general, these are the places where you should comment.
- Functions and methods. Each function and method should have comments that describe its purpose and the algorithm used to accomplish the task. Also write down the assumptions for using the function or method, the meaning of each argument, and whether the function returns a value (because you can’t tell from the function definition).
- Large code blocks. Multiple lines of code that accomplish a single task should be commented at the front to make the task clear.
- Complex algorithms. If you solve a problem in an unusual way, explain it in a note. This can not only help others, but also make yourself faster to remember when you look again in the future.
- Using black technology. Due to browser differences, JavaScript code often contains some sort of hack technology. Don’t assume that other people can see what a hack is meant to solve for a particular browser. If you can’t use a browser in the normal way, state the purpose of the hack in the comments. This prevents people from “fixing” the hack because they think it doesn’t work, only to have the problems you fixed recur.
Indentation and comments make code easier to understand and maintain in the future.
2.2 Naming variables and functions
Proper naming of variables and functions is also critical for readability and maintainability. Since many JavaScript developers come from “rough” backgrounds, it’s easy to name variables with foo, bar, and functions with doSomething. Professional JavaScript developers must break these habits in order to write maintainable code. The following are general rules for naming.
- The variable name should be a noun, such as car or person.
- Function names should start with a verb, such as getName(). Functions that return a Boolean value usually start with is, such as isEnabled().
- Use logical names for both variables and functions without worrying about length. The problem of long names can be solved by post-processing and compression.
- Variables, functions, and methods should start with a lowercase letter and use camel case, such as getName() and isPerson. Class names should start with a capital letter, such as Person, RequestFactory. Constant values should be all caps and followed by an underscore, such as REQUEST_TIMEOUT.
- Use descriptive and intuitive names, but don’t get too long. GetName () looks like it will return the name, while PersonFactory looks like it will generate a Person object or instance.
Avoid useless variable names altogether, such as ones that do not represent the type of contained data. With proper naming, the code reads like a story and is therefore easier to understand.
2.3 Transparency of variable types
Because JavaScript is a loosely typed language, it’s easy to forget what data types a variable contains. Proper naming can go some way to solving this problem, but it is not enough. There are three ways to indicate the data type of a variable.
The first way is through initialization. When you define a variable, you should immediately initialize it to a value of the type you want to use in the future. For example, a variable that wants to hold a Boolean value can be initialized to true or false, while a variable that wants to hold a numeric value can be initialized to a numeric value. Let’s look at a few more examples:
// initialize the type of the variable let found = false; // Boolean let count = -1; // number let name = ""; // string let person = null; // objectCopy the code
A value initialized to a specific data type can explicitly indicate the type of a variable. Prior to ES6, initialization was not appropriate for function arguments in function declarations. After ES6, you can specify default values for parameters in function declarations to indicate their types.
The second way to express variable types is to use Hungarian notation. Hungarian notation means that a variable name is prefixed with one or more characters to indicate the data type. This notation was once very popular in scripting languages and has been the preferred format for JavaScript for a long time. For basic data types, o represents an object, S represents a string, I represents an integer, F represents a float, and B represents a Boolean. Let’s look at some examples.
// Use Hungarian notation to indicate the data type let bFound; // Boolean let iCount; // integer let sName; // string let oPerson; // objectCopy the code
Hungarian notation can also be applied to function arguments. The disadvantage of the Hungarian notation is that it makes the code less readable, less intuitive, and destroys the natural reading fluency of similar sentences. For this reason, Hungarian notation has been abandoned by many developers.
A final way to indicate data types is to use type annotations. Type comments are placed after the variable name and before the initialization expression. The basic idea is to specify the type with a comment next to the variable, such as:
Let found /*:Boolean*/ = false; let count /*:int*/ = 10; let name /*:String*/ = "Nicholas"; let person /*:Object*/ = null;Copy the code
Type annotations inject type information into the code while maintaining overall readability. The disadvantage of type comments is that you can no longer use multi-line comments to comment out large blocks of code. Because type comments are also multi-line comments, they can cause interference, as shown in the following example:
/* let found /*:Boolean*/ = false; let count /*:int*/ = 10; let name /*:String*/ = "Nicholas"; let person /*:Object*/ = null; * /Copy the code
The intention here was to comment out all variable declarations with multi-line comments. But the type annotation interferes because the first /* (line 2) matches the first */ (line 3), resulting in a syntax error. If you want to comment out code that contains type comments, you can only comment out each line line using single-line comments (some editors can do this automatically).
These are the three most common ways to specify variable data types. Each method has advantages and disadvantages, you can choose according to your own situation. The key is to see what works best for your project and ensure consistency.
- Loose coupling
As soon as one part of the application becomes too closely dependent on another, the code becomes strongly coupled and thus difficult to maintain. The typical problem is to reference one object directly to another. So if you change one, you may have to change the other. Tightly coupled software is difficult to maintain and must be rewritten frequently.
Given the technology involved, Web applications can become too coupled in some cases. The key is to be aware of this and always be careful not to have strong coupling in your code.
Decoupling 3.1 HTML/JavaScript
The most common coupling in Web development is HTML/JavaScript coupling. In web pages, HTML and JavaScript represent different layers of solutions. HTML is data, JavaScript is behavior. Because they interoperate, the two technologies need to be linked together in different ways. Unfortunately, some of these ways lead to strong coupling between HTML and JavaScript.
Embedding JavaScript directly into HTML, including the use of
<! <script> document.write("Hello world!") ); </script> <! <input type="button" value="Click Me" onclick="doSomething()"/>Copy the code
While this is technically fine, in practice it leads to tight coupling between HTML data and JavaScript behavior. Ideally, HTML and JavaScript should be completely separate, bringing in JavaScript through external files and then adding behavior using DOM.
In the case of strong coupling between HTML and JavaScript, every time you analyze a JavaScript error, you must first determine whether the error came from HTML or JavaScript. Also, this introduces new errors in code availability. In this example, the user might click the button before the doSomething() function is available, causing JavaScript to report an error. Because every time you change the button’s behavior, you need to change both the HTML and JavaScript, and only the latter is really necessary. This reduces the maintainability of your code.
In the opposite case, HTML and JavaScript also become strongly coupled: the HTML is included in JavaScript. This usually happens when you insert a piece of HTML into a page with innerHTML, as in:
Function insertMessage(MSG) {let container = document.getelementByid ("container"); container.innerHTML = `<div class="msg"> <p> class="post">${msg}</p> <p><em>Latest message above.</em></p> </div>`; }Copy the code
In general, you should avoid creating a lot of HTML in JavaScript. Again, this is mostly about keeping the data layer and the behavior layer separate, making it easier to locate the problem when it goes wrong. If you use the code example above, you can cause a page layout error if you insert the wrong HTML format dynamically. But locating the error in this case is more difficult. Because this is usually the first place to look for the HTML source code in the page that is wrong, but you can’t find it because it is dynamically generated. Also, changing data or pages requires changing JavaScript, which shows that the two layers are tightly coupled.
HTML rendering should be kept as separate from JavaScript as possible. When inserting data using JavaScript, you should insert tags as little as possible. The corresponding markup can be included and hidden in the page, and JavaScript can display it directly when needed, rather than dynamically generating it. Another option is to get the HTML to display through an Ajax request, which also ensures that the same rendering layer (PHP, JSP, Ruby, etc.) is responsible for exporting the markup, rather than embedding the markup in JavaScript.
Decoupling HTML and JavaScript saves troubleshooting time because it is easier to locate the source of the error. Decoupling also helps maintain maintainability, with changes to behavior only involving JavaScript and changes to markup only involving the files to be rendered.
Decoupling 3.2 CSS/JavaScript
Another layer of Web applications is CSS, which is responsible for the appearance of the page. JavaScript and CSS are closely related; they are both built on TOP of HTML and are often used together. As is the case with HTML and JavaScript, CSS can be strongly coupled to JavaScript. The most common examples are using JavaScript to modify individual styles, such as:
// CSS tightly coupled to JavaScript element.style.color = "red"; element.style.backgroundColor = "blue";Copy the code
Since CSS is responsible for the look and feel of the page, any styling problems should be solved through CSS files. However, if JavaScript directly modifies individual styles (such as colors), it adds a factor to consider and even modify when typing errors. The result is that JavaScript takes on the task of page display in a way that is tightly coupled with CSS. If styles are to be changed at some point in the future, both CSS and JavaScript will need to be changed. This is a nightmare for the developer responsible for maintenance. Clear decoupling of layers from layers is required.
Modern Web applications often use JavaScript to change styles, so while it’s not possible to completely decouple CSS and JavaScript, you can make the coupling looser. This can be done by dynamically changing the class name rather than the style, for example:
// CSS loosely coupled with JavaScript element.className = "edit";Copy the code
You can restrict most styles to CSS files by changing the CSS class name of the element. JavaScript is only responsible for changing the class name of the applied style, not directly affecting the style of the element. As long as the class name of the application is correct, the appearance problem is CSS, not JavaScript.
Also, ensuring proper separation between layers is critical. Display problems should only be solved in CSS, behavior problems should only be solved in JavaScript. Loose coupling between these layers improves the maintainability of the entire application.
3.3 Decouple application logic/event handlers
Every Web application has a large number of event handlers listening for events. However, few of them really separate application logic from event handlers. Consider the following example:
function handleKeyPress(event) { if (event.keyCode == 13) { let target = event.target; let value = 5 * parseInt(target.value); if (value > 10) { document.getElementById("error-msg").style.display = "block"; }}}Copy the code
This event handler contains application logic in addition to handling events. The problem with this is twofold. First, there is no way to trigger application logic other than events, which makes debugging difficult. What if the expected results don’t happen? Is the event handler not invoked, or is the application logic faulty? Second, if subsequent events correspond to the same application logic, it will lead to code duplication, otherwise it will have to be extracted into a function. Either way, it leads to extra work that would otherwise be unnecessary.
It is better to separate the application logic from the event handler, each doing its own thing. An event handler should focus on information about an event object and then pass that information to some method that handles the application logic. For example, the previous example could be rewritten like this:
function validateValue(value) { value = 5 * parseInt(value); if (value > 10) { document.getElementById("error-msg").style.display = "block"; } } function handleKeyPress(event) { if (event.keyCode == 13) { let target = event.target; validateValue(target.value); }}Copy the code
After this modification, the application logic is separated from the event handler. The handleKeyPress() function simply checks to see if the user has pressed the enter key (event.keycode equals 13), and if so, obtains the event target and passes its value to the validateValue() function, which handles the application logic. Note that the validateValue() function does not contain any code that relies on event handlers. This function accepts only one value and can then do anything with that value.
There are many benefits to separating application logic from event handlers. First, you can easily modify the events that trigger a process. If the process was triggered by a mouse click and now you want to add a keyboard action, it’s also easy to change. Second, you can test your code without adding events, making it easier to create unit tests and even integrate them with application automation.
Here are some points to keep in mind when decoupling application and business logic.
- Do not pass the Event object to other methods, but pass only the necessary data in the event object.
- Every possible action in the application should be performed without an event handler.
- Event handlers should handle events and leave the subsequent processing to the application logic.
Doing all of these things makes a huge difference to the maintainability of any code, and opens up many possibilities for future testing and development.
- Coding conventions
Writing maintainable JavaScript isn’t just about the code format and specification, it’s also about what the code does. Developing Web applications in large enterprises often requires many people to work together. This is where you need to make sure that everyone’s browser environment has consistent rules. To do this, developers should follow certain coding conventions.
4.1 Respect object ownership
The dynamic nature of JavaScript means you can change almost anything at any time. It has been said in the past that nothing in JavaScript is sacrosanct because you can’t mark anything as final or permanent. But with the introduction of tamper-proof objects in ECMAScript 5, things are different. Of course, objects can still be modified by default. In other languages, objects and classes are immutable without source code. JavaScript allows you to modify any object at any time, which can lead to accidental overwriting of the default behavior. Since the language has no limits, it’s up to the developers to limit themselves.
Perhaps the most important coding convention in enterprise development is to respect object ownership, which means don’t modify objects that don’t belong to you. Simply put, if you are not responsible for creating and maintaining an object, including its constructors or its methods, you should not make any changes to it. To be more specific, it is:
- Do not add attributes to instances or stereotypes
- Do not add methods to instances or prototypes
- Do not redefine existing methods
The problem is that developers assume the browser environment works a certain way. Modifying objects that are used by more than one person means that errors can occur. If one wants a function to be called stopEvent(), used to cancel the default behavior of an event. Then, you change it to remove the default behavior of events and add other event handlers. As you can imagine, problems are bound to follow. Others may think that this function only does the initial thing, and may be used incorrectly or cause damage because of the side effects it adds later.
The above rules apply not only to custom types and objects, but also to native types and objects, such as Object, String, Document, window, and so on. The potential risks are even greater when you consider that browser vendors can also modify these objects in unexpected ways without announcing it.
A similar thing happened with a popular Prototype library. At the time, the library implemented the getElementsByClassName() method on the Document object, returning an instance of Array, which in turn added the each() method. John Resig, the author of jQuery, later analyzed the impact of this problem on his blog. In his blog (johnresig.com/blog/getele…). Note that the problem is caused by browsers natively implementing the same getElementsByClassName() method as well. But Prototype’s method of the same name returns Array instead of NodeList, which has no each() method. Developers using the library have previously written code like this:
document.getElementsByClassName("selected").each(Element.hide);
Copy the code
While this works fine in browsers that don’t natively implement the getElementsByClassName() method, it does in browsers that implement it. Because two methods with the same name return different results. It is impossible to predict how browser vendors will modify native objects in the future, so any modification of them could cause problems at some point in the future when conflicts arise.
The best way to do this is to never modify objects that don’t belong to you. Only those you create are your objects, including custom types and object literals. Array, document, etc., are not yours because they exist before your code executes. You can add new functionality to an object like this:
- Create a new object that contains the functionality you want to interact with someone else’s object.
- Creating a new custom type inherits the type you want to modify and adds new functionality to the custom type.
Many JavaScript libraries currently subscribe to this development philosophy, so that no matter how browsers change, they can evolve and adapt.
4.2 Not declaring global variables
Closely related to respecting object ownership is the minimization of declaring global variables and functions. Again, this is about creating a consistent and maintainable script running environment. You can create at most one global variable as a namespace for other objects and functions. Consider the following example:
// Two global variables -- no! var name = "Nicholas"; function sayName() { console.log(name); }Copy the code
The above code declares two global variables: name and sayName(). They can be contained in an object as follows:
Var MyApplication = {name: "Nicholas", sayName: function() {console.log(this.name); }};Copy the code
This rewritten version declares only one global object, MyApplication. Inside this object, there is name and sayName(). This avoids several problems with previous versions. First, the variable name overrides the window.name attribute, which may affect other functionality. Second, it helps to figure out where functionality is concentrated. Calling myApplication.sayname () logically implies that any problem has occurred and the cause can be found in MyApplication code.
Such a global object can be extended to the concept of a namespace. Namespaces involve creating an object through which capabilities are exposed. Google’s Closure library, for example, uses such namespaces to organize its code. Here are a few examples:
- Goog.string: a method used to manipulate strings.
- Goog.html. utils: Methods related to HTML.
- Goog.i18n: Methods related to internationalization (I18N).
The object GOOG is like a container in which all the other objects are contained. As long as you use objects to organize functionality in this way, you can call that object a namespace. The entire Google Closure library is built on the concept of being able to coexist with other JavaScript libraries on the same page.
The most important thing about namespaces is to decide on a global object name that everyone agrees on. The name should be unique enough not to conflict with others. In most cases, use the developer’s company name, such as GOOG or Wrox. The following example demonstrates using Wrox as a namespace to organize functionality:
Var Wrox = {}; // Create a namespace for the book (Professional JavaScript) wrox.projs = {}; // Add other objects used in this book wrox.projs.eventUtil = {... }; Wrox.ProJS.CookieUtil = { ... };Copy the code
In this case, Wrox is the global variable, and a namespace is created underneath it. If all the code in this book is saved in the wrox.projs namespace, then other authors’ code can be saved using their own objects. As long as everyone follows this pattern, there is no need to worry about someone overwriting EventUtil or CookieUtil here, because even if they are re-named they will only appear in different namespaces. Take the following example:
// Create a namespace for another book (Professional Ajax) wrox. ProAjax = {}; // Add other objects wrox.proajax.eventUtil = {... }; Wrox.ProAjax.CookieUtil = { ... }; / / object can be used as usual ProJS following Wrox ProJS. EventUtil. AddHandler (...). ; / / and ProAjax object Wrox. Below ProAjax. EventUtil. AddHandler (...). ;Copy the code
While namespaces require a little more code, they are well worth the cost from a maintainability perspective. Namespaces ensure that code does not interfere with other code on the page.
4.3 Do not compare NULL
JavaScript does not automatically do any type checking, so it is the developer’s responsibility to do so. As a result, a lot of JavaScript code doesn’t do type checking. The most common type check is to see if a value is null. However, there is too much code to compare to NULL, and much of it frequently throws errors because of inadequate type checking. Take the following example:
function sortArray(values) { if (values ! = null) {// Don't compare! values.sort(comparator); }}Copy the code
The purpose of this function is to sort an array using the given comparison function. The values argument must be an array for the function to execute properly. However, the if statement here simply checks that the value is not NULL. In fact, strings, values, and many other values can pass this check, resulting in an error.
In reality, simply comparing NULL is usually not enough. Checking the type of a value is really checking the type, not what it can’t be. For example, in the previous code, the values argument should be an array. To do this, check to see if it is an array, not null. We can rewrite that function like this:
Function sortArray(values) {if (values instanceof Array) {// select values.sort(comparator); }}Copy the code
This version of the function filters all invalid values without null at all.
If you see code that compares NULL, you can replace it with one of the following techniques.
- If the value should be a reference type, check its constructor using the instanceof operator.
- If the value should be a primitive type, use Typeof to check its type.
- If you want the value to be an object with a particular method name, use the Typeof operator to ensure that an object with the given name exists on the object.
The fewer null comparisons you have in your code, the easier it is to identify the purpose of type checking and eliminate unnecessary errors.
4.4 Using Constants
The goal of constant dependence is to separate the data from the application logic so that it can be modified without raising errors. Strings displayed on the user interface should be extracted in this way to facilitate internationalization. Urls should also be extracted this way, because they are likely to change as the application becomes more complex. Basically, when something like this needs to be changed in the future for one reason or another, you might have to find a function and change the code in it. And every time you change the application logic like this, you can introduce new errors. To do this, you can extract the data that may be modified and place it in a separately defined constant to separate the data from the logic.
The key is to separate the data from the logic that uses them. You can use the following criteria to check what data needs to be extracted.
- Recurring values. Any value that is used more than once should be extracted into the constant. This eliminates the error of changing one value but not the other. CSS class names are also included here.
- User interface string. Any strings that will be displayed to the user should be extracted to facilitate internationalization.
- URL: The addresses of resources in Web applications often change, so it is recommended that all urls be managed in one place.
- Any value that can change. Any time you use a literal in your code, ask yourself if the value might change in the future. If the answer is yes, then it should be extracted into constants.
Using constants is an important technique for enterprise-class JavaScript because it makes code easier to maintain and protects it from data changes.
This article is excerpted from the upcoming issue of JavaScript Advanced Programming (4th edition) and forwarded by permission of the publisher.
Can book information query: www.ituring.com.cn/book/2472.