JavaScript is a flexible and powerful programming language that drives most modern-day web sites and applications. The jQuery library, in particular, has a simple API for traversing and manipulating the DOM. Just choose a your selector, add an event handler, and voilà! Instant UI magic. The ease of jQuery, though, comes with a price. Without much thought in organizing your code, it’s easy to end up with a hot mess of jQuery selector soup — and that’s the last thing we want in a complex application like ours.
In this blog post, I will show you the case for object-oriented design in a functional language like JavaScript, using examples from Wealthfront’s codebase.
JavaScript the Easy Way
Most front-end developers learn JavaScript the “easy” way first. Add an event handler here. Another one there. Chain some methods. Nest anonymous functions a few levels deep. It’s a quick and dirty way of getting things done, but it eventually makes testing, debugging, and adding new features a pain.
Let’s look at an example. Here’s an older piece of our code that leverages Dropbox’s zxcvbn library to assess the strength of a user’s password on each keystroke:
Essentially, every input element with the class zxcvbn
will have the behavior defined above. The input elements should have the data
attributes notify
and confirm
, which refer to the message container and confirmation input elements, respectively. On .assess()
, we grab the password’s value, run some preliminary validations, then pass the value to the zxcvbn
function to give us a password score.
While this chunk of code is fairly simple, it has a few weaknesses in design:
1. There is no separation of concerns. The .assess()
function not only conducts the business logic of password evaluation, but also updates the view with the appropriate message. Ideally, this logic should be handled in two separate functions.
2. It’s hard to overwrite the default configuration. Notice the tight coupling between the DOM elements. The code expects the password input to have data attributes for the notify
and confirm
elements. We can’t specify the DOM elements independently of each other.
3. It doesn’t allow code reuse. If we wanted this logic on another page, we would have to copy and paste it over, or render this script like a partial. But if we did that, we would invoke a new anonymous function on each page load, rather than reuse the same function.
JavaScript the Namespaced Way
One of the ways to untangle spaghetti JavaScript code is to create separate functions for the individual units of logic and define them on a common namespace. This allows us to call the functions anywhere within our application.
Let’s see how this looks using a different part of our password code, defined on the wfApp.settings.password
namespace. When a user changes their password, we validate their input and show the response from the server:
This logic is clearly separated out into discrete functions, which makes reading and testing the code easier. Notice, though, that we still have selectors such as $('#password-form')
scattered throughout the code.
There are a few ways to remedy this:
1. Update the functions to refer to this.form
instead. We already have the property this.form
that refers to $('#password-form')
, and using that prevents us from diving into the DOM more times than necessary. The value of this.form
could also be specified as an argument in a separate function. However, this means we would need to invoke that function on every page that uses this component; otherwise, this.form
could still point to the jQuery object from a different view.
2. Pass the the selector or jQuery object as a param. This would allow us to apply the functions to any element we pass in. But this may seem unnecessarily redundant, especially since we use the same set of functions to manipulate the same DOM element.
If you look carefully, you’ll see that the code actually has the beginnings of objected-oriented design. After all, wfApp.settings.password
is an object and maintains states like this.form
and this.submitUrl
across its functions. However, it’s not quite object-oriented because wfApp.settings.password
is an object literal, which means there’s only one instance of it — and that’s our problem.
With just a few tweaks, we can flip this code into a more object-oriented model to address our configurability issues.
JavaScript the Object-Oriented Way
While JavaScript is a popular language, its full object-oriented programming capabilities are often underutilized. JavaScript doesn’t have the concept of a class, but we can create objects with the same properties using constructor functions. The instances inherit the same methods defined on the prototype.
If we begin to think of our DOM elements and data more as concrete objects with set methods, then we can reorganize our code into something reusable and configurable.
Creating an Object Constructor
As most of our password code deals with view logic, let’s make a constructor for a new object called PasswordView
:
This constructor should be the place where we create our jQuery objects. Since we want the selectors to be configurable, the function should consume a hash with the desired selectors and create the jQuery objects accordingly.
It should look something like this:
Here, we have three elements that belong to PasswordView
:
- $el
, which refers to the form element
- $inputCurrent
, which refers to the input for the current password
- $inputNew
, which refers to the inputs for the new password and confirmation
Now we can create actual PasswordView
objects by calling new wfApp.settings.PasswordView()
and passing in a hash of options.
It should look like this:
Because it’s a constructor function, we can create multiple PasswordView
objects each with custom selectors.
Replacing Other jQuery Selectors
In the other functions, we should now replace the instances of $('#password-form')
and the like with our new properties. For instance, we currently have this for the .submit()
function:
Lines 6 and 7 refer to $('#password-form input[name=new], #password-form input[name=verify]')
. Let’s replace that with our new property this.$inputNew
. Also, line 10 refers to $('#password-form input')
. Let’s replace that with this.$el.find('input')
.
With our changes, it should look like this:
Adding Methods to the Object’s Prototype
Let’s move all of the current functions to be defined on PasswordView
’s prototype. This means that all the PasswordView
instances will have methods that point to the same function on the prototype.
Our .submit()
function should now be defined as follows:
Identifying Other Objects
We now have a concrete PasswordView
with easily configurable selectors, but are there any other objects that appear in our code? Since we are passing password-related information to the server, we can encapsulate that information into its own object, which can conveniently just call Password
. Let’s create its constructor function with properties that correspond to the form’s input fields:
What methods should Password
have? One thing that comes to mind is the validation logic. Right now, PasswordView
runs the validation, but that responsibility should belong on the model, since it’s a data-related check. So let’s move .validate()
onto Password
’s prototype:
We want the view to update the text based on the model’s validation result. So let’s pass in the model as one of the view’s option and grab its validation result on submit.
The other advantage of creating a separate Password
object is the ability for our PasswordView
object to use different Password
objects with different constraints. If we had a Password
object with different validation logic, we could easily pull that in without affecting our view logic.
With our new objects and methods defined on the prototype, our code should look like this:
We can see the advantages of this approach:
1. There is a clear separation of concerns. Password
handles the validation logic, while PasswordView
simply shows the text of the validation result.
2. It’s easy to overwrite the default configuration. PasswordView
doesn’t assume any default selectors, so we can pass in any form element of our choosing and just define it once.
3. The same code can be used across multiple views. All we have to do is create new Password
and PasswordView
objects on the pages that require these components. All we need is something like this at the bottom of our markup:
Final Thoughts
As JavaScript is such a flexible language, there are many ways to write JavaScript code, ranging from functional to object-oriented programming to a hybrid of the two. Personally, I am apt toward the object-oriented approach, as it’s easier to track the DOM elements in play and reuse logic across different views. If rolling your own object-oriented code seems ambitious, using a JavaScript framework like Backbone, Ember, or Angular can help you set up an object-oriented web application. However, as you can see, you can also just use jQuery and vanilla JavaScript to achieve the same result.