Home Notes Web development JavaScript: small details

JavaScript: small details E-mail
Saturday, 11 June 2011 21:29

I was thinking of alternative ways to define a custom class in JavaScript, and came up with two implementations.  I would like to share what I came up with, to demonstrate the flexibility of JavaScript as well as to highlight a couple different ways of accomplishing the same purpose.

Exhibits

Introduction

In what I've called Design 1 and Design 2, there is a different architecture of the Scanner class.  Both implementations return the same result, with the same structure (shown below).  Also in Design 2 I made several small improvements over Design 1 in the code that serves the same purpose between the two designs but is implemented differently in each.

 

The Scanner class:

  1. Has a single point of entry to start a scanning process, which
  2. Scans a web page (the DOM) for categories of posts, and
  3. For each category, it scans and retrieves the following properties of each post and saves it to a Post object:
    • Title
    • Image
    • URL
    • Rating
  4. Several objects, one for each category scanned, are returned with the properties:
    • Name of category
    • Array of Post objects
  5. There is a special category called "Recent edits", which is scanned separately
    • The posts in this category have another property, "Last edited", which is stored in a wrapper along with  the Post

The returned objects look like:

Scanner class return object

Scanner class code

Here is the actual code for the alternative class designs.  You may want to copy the code elsewhere to view the two versions side by side.

Discussion

Architecture

I'll admit that the design difference between the two models is slight, but it's an important one.  It can be summed up in one statement: In Design 1, the methods perform at the class-level, whereas in Design 2, all methods are declared on the prototype and perform on instances of the Scanner class.

 

Despite the difference being a small one, for clarity's sake I decided to map out the two designs in mind maps, shown below.  (Unfortunately you'll need to manually expand the nodes to see them.)  I asked around to see which is better, and for the scope of this small and simplified problem the answer is "it doesn't really matter."  However, if the problem were a larger, more realistic one, Design 2 seems more suitable.

 

Design 1

 

 

Design 2

 

Optimizations

This is the more interesting part, for me at least.  It's easy to forget about the small optimizations you can make in your code when you're just trying to get it to work in the first place.

 

I'm going to step through the various sections of the code in both designs, and highlight what's better in Design 2.

Design 1

//Start Scanning process
Scanner.startScanning = function() {
  Scanner.getCategories();
   $(categories).each(function() {
     Scanner.getPostsByCategory(this);
   });
   Scanner.getRecentPosts();
}

Design 2

Scanner.fn.startScanning = function() {
   var categories = this.getCategories();
   var context = this;
   for (var i = 0, j = categories.length; i < j; i++) {
     this.getPostsWithCategory($(categories[i]));
  }
  this.getRecentPosts();
 
};

  • $.each is an expensive process, so Design 2 uses a simple for loop instead

Design 1

//Return jQuery object of all post categories
Scanner.getCategories = function() {
  categories = $(// some jQuery selector);
}

Design 2

Scanner.fn.getCategories = function() {
   return $(// some jQuery selector);
}

  • Declaring a variable without the var keyword (like categories above in Design 1) makes it global, which is never a good idea unless you specifically need a global variable
  • While both designs do basically the same thing here, Design 2 adheres to functional programming principles, in that it doesn't have any side effects

Design 1

Scanner.getPostsByCategory = function(category) {
  var scanned = new Scanner();
  scanned.category = Scanner.getCategoryName(category);
  $(category).find(// some jQuery selector).each(function() {
    var post = Scanner.createPost(this);
    scanned.posts.push(post);
  });
  return scanned;
}

Design 2

Scanner.fn.getPostsWithCategory = function(jSingleCategory) {
  var context = this;
  context.category = context.getCategoryName(jSingleCategory);
  jSingleCategory.find(// some jQuery selector).each(function() {
    var post = context.createPost(this);
    context.posts.push(post);
  });
}

  • jQuery ($) lookups are expensive as well, so instead of using $(category) in multiple places like in Design 1, in Design 2 we instead pass $(categories)[i] above in startScanning() so that it gets cached, and can be quickly reused as jSingleCategory

Design 1

//Gather post data - title, URL, image, rating
Scanner.createPost = function(post) {
  var title = Scanner.getTitle(post);
  var image = Scanner.getImage(post);
  var url = Scanner.getUrl(post);
  var rating = Scanner.getRating(post);
  var post = new Post(title, image, url, rating);
  return post;
}
 
 
 
/*********************
Post class
*********************/
 
//Post constructor
var Post = function(title, image, url, rating) {
  this.title = title;
  if (image != null) {
    this.image = image;
  }
  if (url != null) {
    this.url = url;
  }
  if (rating != null) {
    this.rating = rating;
  }
}

Design 2

Scanner.fn.createPost = function(post) {
  var postObj = {};
  var jPost = $(post);
  postObj.title = this.getTitle(jPost);
  postObj.image = this.getImage(jPost);
  postObj.url = this.getUrl(jPost);
  postObj.rating = this.getRating(jPost);
  return post;
}

  • Since the Post object is doing nothing except storing a Post, it is overkill to declare a class for it.  As Design 2 does above, it's enough to just create a Post object literal in the Scanner class.