Let's Make a Framework: Feature Detection

2010-10-07 00:00:00 +0100 by Alex R. Young

Welcome to part 33 of Let's Make a Framework, the ongoing series about
building a JavaScript framework.

If you haven't been following along, these articles are tagged with
lmaf. The project we're creating is called Turing.

Last week I wrote about packaging the project using Node scripts. This week I'm going to
talk about feature detection.


The selector engine we built for the core of turing.dom was
based on the way Firefox interprets CSS selectors. I liked the approach
for the context of these tutorials, because it's a very pragmatic
approach that's easy to follow.

Browsers have been shipping with
querySelectorAll for a while, which reduces the amount of work required to implement DOM

That means turing.dom.get could be rewritten:

// Original code minus all the
// magic in the Searcher class and tokenizer
dom.get = function(selector, root) {
  var tokens = dom.tokenize(selector).tokens,
      searcher = new Searcher(root, tokens);
  return searcher.parse();

// New selector API
dom.get = function(selector) {
  return document.querySelectorAll(selector);

But not all browsers support this yet, so let's check if it's available:

dom.get = function(selector) {
  var root = typeof arguments[1] === 'undefined' ? document : arguments[1];
  if ('querySelectorAll' in document) {
    return root.querySelectorAll(selector);
  } else {
    return get(selector, root);

In the Wild

jQuery will check for querySelectorAll, but it'll only use
it under certain conditions. This is from jQuery 1.4.2:

if (document.querySelectorAll ) {
    var oldSizzle = Sizzle, div = document.createElement("div");
    div.innerHTML = "";

    // Safari can't handle uppercase or unicode characters when
    // in quirks mode.
    if ( div.querySelectorAll && div.querySelectorAll(".TEST").length === 0 ) {

    Sizzle = function(query, context, extra, seed){
      context = context || document;

      // Only use querySelectorAll on non-XML documents
      // (ID selectors don't work in non-HTML documents)
      if ( !seed && context.nodeType === 9 && !isXML(context) ) {
        try {
          return makeArray( context.querySelectorAll(query), extra );
        } catch(e){}

      return oldSizzle(query, context, extra, seed);

    for ( var prop in oldSizzle ) {
      Sizzle[ prop ] = oldSizzle[ prop ];

    div = null; // release memory in IE

The use of an element for capability detection is common in frameworks
-- sometimes it's the only reliable way of detecting a browser's behaviour.

It's a little bit different in Dojo 1.5, but the same Safari issue is

  // some versions of Safari provided QSA, but it was buggy and crash-prone.
  // We need te detect the right "internal" webkit version to make this work.
  var wk = "WebKit/";
  var is525 = (
    d.isWebKit &&
    (nua.indexOf(wk) > 0) &&
    (parseFloat(nua.split(wk)[1]) > 528)

  // IE QSA queries may incorrectly include comment nodes, so we throw the
  // zipping function into "remove" comments mode instead of the normal "skip
  // it" which every other QSA-clued browser enjoys
  var noZip = d.isIE ? "commentStrip" : "nozip";

  var qsa = "querySelectorAll";
  var qsaAvail = (
    !!getDoc()[qsa] &&
    // see #5832
    (!d.isSafari || (d.isSafari > 3.1) || is525 )

  //Don't bother with n+3 type of matches, IE complains if we modify those.
  var infixSpaceRe = /n\+\d|([^ ])?([>~+])([^ =])?/g;
  var infixSpaceFunc = function(match, pre, ch, post) {
    return ch ? (pre ? pre + " " : "") + ch + (post ? " " + post : "") : /*n+3*/ match;

  var getQueryFunc = function(query, forceDOM){
    //Normalize query. The CSS3 selectors spec allows for omitting spaces around
    //infix operators, >, ~ and +
    //Do the work here since detection for spaces is used as a simple "not use QSA"
    //test below.
    query = query.replace(infixSpaceRe, infixSpaceFunc);

      // if we've got a cached variant and we think we can do it, run it!
      var qsaCached = _queryFuncCacheQSA[query];
      if(qsaCached && !forceDOM){ return qsaCached; }

    // Snip

Here the browser and version are derived from the user agent string.


I recently wrote about has.js
which is a clever little project that contains a library of feature
detection tests.

Browser sniffing and feature inference are flawed techniques for detecting browser support in client side JavaScript. The goal of has.js is to provide a collection of self-contained tests and unified framework around using pure feature detection for whatever library consumes it.

Using has.js as inspiration, we should be able to rewrite the previous
code like this:

dom.get = function(selector) {
  var root = typeof arguments[1] === 'undefined' ? document : arguments[1];
  return turing.detect('querySelectorAll') ?
    root.querySelectorAll(selector) : get(selector, root);

Feature Detection Implementation

Making a library of feature tests is fairly easy with a plain
Object. Tests can be referred to by name and easily looked
up at runtime. Also, a cache can be used to store the results of the

The core of this functionality is something inherent to JavaScript
programming rather than just browser-related, so I put this in

var testCache = {},
    detectionTests = {};

turing.addDetectionTest = function(name, fn) {
  if (!detectionTests[name])
    detectionTests[name] = fn;

turing.detect = function(testName) {
  if (typeof testCache[testCache] === 'undefined') {
    testCache[testName] = detectionTests[testName]();
  return testCache[testName];

The results are cached because they should only be run once. This type
of capability detection is intended to be used against the environment,
rather than features that might load dynamically in runtime, so I think
it's safe to run the tests once.

Then the jQuery-inspired querySelectorAll test can be added

turing.addDetectionTest('querySelectorAll', function() {
  var div = document.createElement('div');
  div.innerHTML = '';

  // Some versions of Safari can't handle uppercase in quirks mode
  if (div.querySelectorAll) {
    if (div.querySelectorAll('.TEST').length === 0) return false;
    return true;

  // Helps IE release memory associated with the div
  div = null;
  return false;


Looking through popular JavaScript frameworks made me realise that most
of them still use the user agent string to determine what capabilities
are available. This might be fine most of the time, but I don't consider
it best practice. The way jQuery tests capabilities using techniques
like the dummy DOM element creation seems like a lot of work, but it
relies on browser behaviour rather than the user agent string.

I originally wanted to write this article about
querySelectorAll and its family of related methods, but
adding generic capability detection support became an interesting little
rabbit hole. I hope this illustrates how simple tasks can require extra
effort to make code highly reusable.