JavaScript - - By Aurelio De Rosa

Five Useful Functions Missing in Javascript

Related Topics:

JavaScript has been around for several years and its core continues to mature, including new classes and functions to help programmers do their job. However, some basic utility functions are still missing and instead implemented with libraries like jQuery, Prototype and MooTools. Working with these tools is great but in some cases their use is excessive for your needs. This article covers five useful functions that I have always thought belonged in JavaScript.

getElementsByClassName()

JavaScript allows an element to be retrieved using its id with the function getElementById(), but prior of HTML5 there was no native function to get one or more elements using a class name. The new function is called getElementsByClassName(), and it is available in Firefox 3+, Opera 9.5+, Safari 3.1+, and all versions of Google Chrome. Unfortunately, as you might guess, it isn’t available in all of the versions of Internet Explorer, the number one enemy browser for web designers. Only Internet Explorer 9+ supports getElementsByClassName(), so for older versions you need a wrapper function.

The best function I’ve found was written by Robert Nyman. His implementation is released under the MIT license, and it has been recommended by the WHATWG. It uses the native getElementsByClassName() method in browsers that support it, then falls back to the little-known document.evaluate() method, which is supported by older versions of Firefox (since at least 1.5) and Opera (since at least 9.27). If all else fails, the script falls back to recursively traversing the DOM and collecting elements that match the given classnames. You can find the code below and on his repository.

var getElementsByClassName = function (className, tag, elm){
  if (document.getElementsByClassName) {
    getElementsByClassName = function (className, tag, elm) {
      elm = elm || document;
      var elements = elm.getElementsByClassName(className),
      nodeName = (tag)? new RegExp("\b" + tag + "\b", "i") : null,
      returnElements = [],
      current;
      for(var i=0, il=elements.length; i<il; i+=1){
        current = elements[i];
        if(!nodeName || nodeName.test(current.nodeName)) {
          returnElements.push(current);
        }
      }
      return returnElements;
    };
  }
  else if (document.evaluate) {
    getElementsByClassName = function (className, tag, elm) {
      tag = tag || "*";
      elm = elm || document;
      var classes = className.split(" "),
      classesToCheck = "",
      xhtmlNamespace = "http://www.w3.org/1999/xhtml",
      namespaceResolver = (document.documentElement.namespaceURI === xhtmlNamespace)? xhtmlNamespace : null,
                           returnElements = [], elements, node;
      for(var j=0, jl=classes.length; j<jl; j+=1){
        classesToCheck += "[contains(concat(' ', @class, ' '), ' " + classes[j] + " ')]";
      }
      try {
        elements = document.evaluate(".//" + tag + classesToCheck, elm, namespaceResolver, 0, null);
      }
      catch (e) {
        elements = document.evaluate(".//" + tag + classesToCheck, elm, null, 0, null);
      }
      while ((node = elements.iterateNext())) {
        returnElements.push(node);
      }
      return returnElements;
    };
  }
  else {
    getElementsByClassName = function (className, tag, elm) {
      tag = tag || "*";
      elm = elm || document;
      var classes = className.split(" "),
          classesToCheck = [],
          elements = (tag === "*" && elm.all)? elm.all : elm.getElementsByTagName(tag),
          current,
          returnElements = [],
          match;
      for(var k=0, kl=classes.length; k<kl; k+=1){
        classesToCheck.push(new RegExp("(^|\s)" + classes[k] + "(\s|$)"));
      }
      for(var l=0, ll=elements.length; l<ll; l+=1){
        current = elements[l];
        match = false;
        for(var m=0, ml=classesToCheck.length; m<ml; m+=1){
          match = classesToCheck[m].test(current.className);
          if (!match) {
            break;
          }
        }
        if (match) {
          returnElements.push(current);
        }
      }
      return returnElements;
    };
  }
  return getElementsByClassName(className, tag, elm);
};

extend()

If you have ever written a plugin, you have almost certainly faced the problem of merging two or more objects. This often happens when you have some default settings and want the user to be able to replace some of the default values. If you were using jQuery, you could use extend(), but since we’re talking about raw JavaScript, the bad news is that there’s no native function. Luckily, you can easily build it yourself. The following example will show you how to create code that works the same way as the jQuery method. I add our extend() method to the Object prototype so that all objects can share the same method.

Object.prototype.extend = function() {
  if (arguments.length === 0)
    return this;

  for (var i = 0; i < arguments.length; i++) {
    for (var property in arguments[i]) {
      if (arguments[i].hasOwnProperty(property))
        this[property] = arguments[i][property];
    }
  }
  return this;
};

This function accepts a variable number of arguments. This is possible thanks to the use of arguments, a local array-like object available inside every function. If you need more information about arguments, I advice you to read arguments: A JavaScript Oddity.

equals()

Object comparisons are a very common operation. While this test can be done using the strict equality operator (===), sometimes you don’t want to test if two variables refer to the same object in memory. Instead, you want to know if two objects have the same properties with the same values. The code below does exactly this. Please note that the following code is not mine; it belongs to a user called crazyx. Again, equals() has been added to the Object.prototype.

Object.prototype.equals = function(x) {
  var p;
  for(p in this) {
    if (typeof(x[p]) == "undefined")
      return false;
  }
  for(p in this) {
    if (this[p]) {
      switch(typeof(this[p])) {
        case "object":
          if (!this[p].equals(x[p]))
            return false;
          break;
        case "function":
          if (typeof(x[p]) == "undefined" ||
             (p != "equals" && this[p].toString() != x[p].toString()))
            return false;
          break;
        default:
          if (this[p] != x[p])
            return false;
      }
    }
    else {
      if (x[p])
        return false;
    }
  }
  for(p in x) {
    if(typeof(this[p])=="undefined")
      return false;
  }
  return true;
}

inArray()

JavaScript doesn’t have a native method to test if a value is in an array. We’ll write a function that, as you might expect, will return true if the value is present and false otherwise. This function simply does an identity comparison of the given value against every element of the array. Just like the previous two examples, inArray() is added to the prototype property of the Array class.

Array.prototype.inArray = function (value) {
  for (var i = 0; i < this.length; i++) {
    if (this[i] === value)
      return true;
  }
  return false;
};

This function, due to its simplicity, in many cases doesn’t work as you might expect. Although it works well for basic types like String and Numbers, if you compare objects, it only returns true if the function finds the same object. To better understand how it works, let’s look at the following example.

var array = [1, 2, 3];
console.log(array.inArray(2)); // print true

var obj = {"prop": "value"};
array = [{"prop": 1}, {"prop": "a long string"}, {"prop": "value"}];
console.log(array.inArray(obj)); // print false, not true as you might expect

array = [{"prop": 1}, {"prop": "a long string"}, obj];
console.log(array.inArray(obj)); // print true

The function presented can be enhanced with the help of the equals() function discussed previously. In this way we can get a match if two objects have the same properties and values. Another improvement we can make is to have the function return the position of the element instead of simply true or false. The final version of the function is shown below.

Array.prototype.inArray = function (value) {
  for (var i = 0; i < this.length; i++) {
    if (typeof value === "object") {
      // If both are objects, uses the equals function
      if (typeof this[i] === "object" && value.equals(this[i]))
          return i;
    }
    else if (this[i] === value)
      return i;
  }
  return false;
};

Now, if you run again the examples above, you will get:

1
2
2

toggleClass()

Another method which is often used in jQuery is toggleClass(). It adds or removes a class from an element depending if the class name is already present or not. A simple version of toggleClass() is shown below.

function toggleClass(id, className) {
  var element = document.getElementById(id);
  var classes = element.className.split(/s+/);
  var length = classes.length;

  for(var i = 0; i < length; i++) {
    if (classes[i] === className) {
      classes.splice(i, 1);
      break;
    }
  }
  // The className is not found
  if (length === classes.length)
    classes.push(className);

  element.className = classes.join(" ");
}

This code can also be improved. The for loop in the function does nothing but search the classes array. We can replace the loop with a call to the inArray() function, resulting in the following code.

function toggleClass(id, className) {
  var element = document.getElementById(id);
  var classes = element.className.split(/s+/);
  var length = classes.length;
  var found = classes.inArray(className);
  if (found !== false)
    classes.splice(found, 1);
  // The className is not found
  if (length === classes.length)
    classes.push(className);
  element.className = classes.join(" ");
}

Conclusion

This article highlighted what I think are some of the most important functions missing from JavaScript. Of course, JavaScript misses other things, as we’ll see in the next weeks. But, for now, I’d like to point out the following:

  • Frameworks like jQuery have a lot of useful functions but they add an overhead. So, if you need just a couple of functions, use raw JavaScript and group the methods you need in an extenal file.
  • If you used a function that has been introduced in a new JavaScript version, don’t get rid of it. Wrap it with a conditional statement to test if it’s supported and if not, use your old code as pointed out for getElementsByClassName(). This way you’ll continue to support old browsers.
  • When possible add functions to the prototype of an object as shown for extend(). All of the instances will share the same method and you’ll have better performance.
  • When possible, reuse your code as shown in the second version of toggleClass().
Sponsors