Introduction of Maintainable Javascript: Programming Practices
Bài đăng này đã không được cập nhật trong 9 năm
5. Loose Coupling of UI Layers
5.1. What Is Loose Coupling?
Loose coupling is achieved when you’re able to make changes to a single component without making changes to other components. Loose coupling is essential to the maintainability of large systems for which more than one person is responsible for the development and maintenance of code. You absolutely want developers to be able to make changes in one area of the code without breaking what other developers are doing in a different area of code.
Loose coupling is achieved by limiting each component’s knowledge of the larger system. In essence, every component needs to be as dumb as possible to ensure loose coupling. The less a component knows, the better off the entire system tends to be.
5.2. How to Loose Coupling?
• Keep JavaScript Out of CSS
• Keep CSS Out of JavaScript
• Keep JavaScript Out of HTML
• Keep HTML Out of JavaScript
- Alternative #1: Load from the Server
- Alternative #2: Simple Client-Side Templates
- Alternative #3: Complex Client-Side Templates
6. Avoid Globals
6.1. The Problems with Globals
• Naming Collisions
• Code Fragility
• Difficulty Testing
6.2. Accidental Globals
6.2.1. Problems
One of the more insidious parts of JavaScript is its capacity for creating globals accidentally. When you assign a value to variable that has not previously been defined in a var
statement, JavaScript automatically creates a global variable. For example:
function doSomething() {
var count = 10;
title = "Maintainable JavaScript"; // Bad: global
}
This code represents a very common coding error that accidentally introduces a global variable. The author probably wanted to declare two variables using a single var
statement but accidentally included a semicolon after the first variable instead of a comma. The result is the creation of title
as a global variable.
6.2.2. Avoiding Accidental Globals
• Use tools like JSLint and JSHint
• Use strict mode
6.3. The One-Global Approach
The idea behind the one-global approach is to create a single global with a unique name (one that is unlikely to be used by native APIs) and then attach all of your functionality onto that one global. So instead of creating multiple globals, each would-be global simply becomes a property of your one global
6.3.1. Namespaces
A namespace is simply a logical grouping of functionality under a single property on the global. Grouping functionality into namespaces brings some order to your one global object and allows team members to understand where new functionality belongs as well as where to look for existing functionality.
6.3.2. Modules
A module is a generic piece of functionality that creates no new globals or namespaces. Instead, all of the code takes place within a single function that is responsible for executing a task or publishing an interface. The module may optionally have a name and a list of module dependencies.
Modules aren’t formally part of JavaScript. There is no module syntax (at least, not until ECMAScript 6), but there are some common patterns for creating modules. The two most prevalent types are YUI modules and Asynchronous Module Definition (AMD) modules.
6.4. The Zero-Global Approach
It is possible to inject your JavaScript into a page without creating a single global variable. This approach is quite limited, so it is useful only in some very specific situations. The most common situation is with a completely standalone script that doesn’t have to be accessed by any other scripts. This situation may occur because all of the necessary scripts are combined into one file, or because the script is small and being inserted into a page that it shouldn’t interfere with. The most common use case is in creating a bookmarklet.
Bookmarklets are unique in that they don’t know what’s going to be on the page and don’t want the page to know that they are present. The end result is a need for a zero global embedding of the script, which is accomplished by using an immediate function invocation and placing all of the script inside of the function.
3. Event Handling
• Rule #1: Separate Application Logic • Rule #2: Don’t Pass the Event Object Around
4. Avoid Null Comparisons
• Detecting Primitive Values using typeof
• Detecting Reference Values using instance
• Detecting Functions using typeof
• Detecting Arrays using Array.isArray()
• Detecting Properties using in
9. Separate Configuration Data from Code
9.1. What Is Configuration Data?
Configuration data is any hardcoded value in an application.
The following are all examples of configuration data:
• URLs
• Strings that are displayed in the UI
• Repeated unique values
• Settings (i.e., items per page) Any value that may change
9.2. Externalizing Configuration Data
The first step in separating configuration data from code is to externalize the configuration data, which means getting the data out of the middle of your JavaScript code.
9.3. Storing Configuration Data
Configuration data is best stored in a separate file to create a clean separation between it and application logic. A good starting point is to have a separate JavaScript file for configuration data. One of my favorite formats for configuration data is a Java properties file. The next step is to convert this file into something that’s usable by JavaScript.
10. Throw Your Own Errors
10.1. The Nature of Errors
An error occurs in programming when something unexpected happens. Maybe the incorrect value was passed into a function or a mathematical operation had an invalid operand. Programming languages define a base set of rules that when deviated from, result in errors so that the developer can fix the code. Debugging would be nearly impossible if errors weren’t thrown and reported back to you. If everything failed silently, it would take you a long time to notice that there was an issue in the first place, let alone isolate and fix it. Errors are the friends of developers, not enemies.
10.2. Throwing Errors in JavaScript
You can throw an error by using the throw operator and providing an object to throw.
throw new Error("Something bad happened.”)
10.3. Advantages of Throwing Errors
Throwing your own error allows you to provide the exact text to be displayed by the browser.
10.4. When to Throw Errors
The best place for throwing errors is in utility functions, such as the addClass()
function, that is a general part of the scripting environment and may be used in any number of places, which is precisely the case with JavaScript libraries. All JavaScript libraries should throw errors from their public interfaces for known error conditions.
Some good general rules of thumb for throwing errors:
• Once you’ve fixed a hard-to-debug error, try to add one or two custom errors that can help you more easily the solve the problem, should it occur again.
• If you’re writing code and think, “I hope [something] doesn’t happen—that would really mess up this code,” then throw an error when that something occurs.
• If you’re writing code that will be used by people you don’t know, think about how they might incorrectly use the function and throw errors in those cases.
Remember that the goal isn’t to prevent errors—it’s to make errors easier to debug when they occur.
10.5. The try-catch Statement
JavaScript provides a try-catch
statement that is capable of intercepting thrown errors before they are handled by the browser. The code that might cause an error comes in the try
block and code that handles the error goes into the catch
block.
try {
somethingThatMightCauseAnError();
} catch (ex) {
handleError(ex);
}
There is also a finally clause that can be added. The finally clause contains code that will be executed regardless of whether an error occurs. For example:
try {
somethingThatMightCauseAnError();
} catch (ex) {
handleError(ex);
} finally {
continueDoingOtherStuff();
}
10.6. Throw or try-catch?
Errors should be thrown only in the deepest part of the application stack, which, as discussed previously, typically means JavaScript libraries. Any code that handles application-specific logic should have error-handling capabilities and should therefore be catching errors thrown from the lower-level components.
Application logic always knows why it was calling a particular function and is therefore best suited for handling the error. Never have a try-catch statement with an empty catch clause; you should always be handling errors in some way.
If you know an error might happen, then you should also know how to recover from that error. Exactly how you recover from the error may be different in development mode as opposed to what actually gets put into production, and that’s okay. The important thing is that you’re actually handling the error, not just ignoring it.
11. Don’t Modify Objects You Don’t Own
11.1. What Do You Own?
You own an object when your code creates the object. The code that creates the object may not have necessarily been written by you, but as long as it’s the code you’re responsible for maintaining, then you own that object. For instance, the YUI team owns the YUI object, and the Dojo team owns the dojo object. Even though the original person who wrote the code defining the object may not work on it anymore, the respective teams are still the owners of those objects.
When you use a JavaScript library in a project, you don’t automatically become the owner of its objects. In a multiple-developer project, everyone is assuming that the library objects work as they are documented. If you’re using YUI and make modifications to the YUI object, then you’re setting up a trap for your team. Someone is going to fall in, and it’s going to cause a problem.
Remember, if your code didn’t create the object, then it’s not yours to modify, which includes:
• Native objects (Object, Array, and so on)
• DOM objects (for example, document)
• Browser Object Model (BOM) objects (such as window)
• Library objects
11.2. The Rules
• Don’t override methods.
• Don’t add new methods.
• Don’t remove existing methods.
11.3. Better Approaches
• Object-Based Inheritance
• Type-Based Inheritance
• The Facade Pattern
• Preventing Modification
12. Browser Detection
• User-Agent Detection
• Feature Detection
• Avoid Feature Inference
• Avoid Browser Inference
12.1. What Should You Use?
Feature inference and browser inference are very bad practices that should be avoided at all costs. Straight feature detection is a best practice, and in almost every case, is exactly what you’ll need. Typically, you just need to know if a feature is implemented before using it. Don’t try to infer relationships between features, because you’ll end up with false positives or false negatives.
The author doesn't say never use user-agent detection, because I do believe there are valid use cases. I don’t believe, however, that there are a lot of valid use cases. If you’re thinking about user-agent sniffing, keep this in mind: the only safe way to do so is to target older versions of a specific browser. You should never target the most current browser version or future versions.
His recommendation is to use feature detection whenever possible. If it’s not possible, then fall back to user-agent detection. Never, ever use browser inference, because you’ll be stuck with code that isn’t maintainable and will constantly require updating as browsers continue to evolve.
All rights reserved